From 5d424052c79038363962922304712d5c406c1596 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 17:16:39 -0600 Subject: [PATCH 01/15] Simplify min / max implementation. We need to decide if this is strictly numeric. --- defaultMethods.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index c3d5458..bc495c0 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, @@ -705,20 +717,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 From 9303e8cab30c21019a5c924e900fbfd716de3a9d Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 19:11:25 -0600 Subject: [PATCH 02/15] Implement precision mechanism for JSON Logic; I'll probably release this soon. --- defaultMethods.js | 3 ++ logic.js | 16 ++++--- optimizer.js | 2 +- precision/index.js | 98 ++++++++++++++++++++++++++++++++++++++++++ precision/package.json | 10 +++++ precision/scratch.js | 13 ++++++ precision/yarn.lock | 8 ++++ 7 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 precision/index.js create mode 100644 precision/package.json create mode 100644 precision/scratch.js create mode 100644 precision/yarn.lock diff --git a/defaultMethods.js b/defaultMethods.js index bc495c0..78bbf70 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -947,6 +947,9 @@ defaultMethods.var.compile = function (data, buildState) { // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods.var.optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true +// @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations +defaultMethods['<'].precise = defaultMethods['<='].precise = defaultMethods['>'].precise = defaultMethods['>='].precise = defaultMethods['=='].precise = defaultMethods['==='].precise = defaultMethods['!='].precise = defaultMethods['!=='].precise = true + export default { ...defaultMethods } diff --git a/logic.js b/logic.js index 45e8a0e..0475c41 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,11 +150,14 @@ 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 } - if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above) + if (logic && typeof logic === 'object') { + if (this.precision && !options.precise && logic.toNumber) return Number(logic) + if (Object.keys(logic).length > 0) return this._parse(logic, data, above) + } return logic } 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/precision/index.js b/precision/index.js new file mode 100644 index 0000000..e14605f --- /dev/null +++ b/precision/index.js @@ -0,0 +1,98 @@ +export function configurePrecision (engine, constructor) { + engine.precision = constructor + engine.addMethod('+', { + method: (data) => { + if (typeof data === 'string') return constructor(data) + if (typeof data === 'number') return constructor(data) + let res = 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`(engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.plus(${args[i]}))` + return res + } + return false + }, + optimizeUnary: true, + deterministic: true, + precise: true + }) + + engine.addMethod('-', { + method: (data) => { + if (typeof data === 'string') return constructor(data).mul(-1) + if (typeof data === 'number') return constructor(data).mul(-1) + let res = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.minus(${args[i]}))` + return res + } + return false + }, + optimizeUnary: true, + deterministic: true, + precise: true + }) + + engine.addMethod('*', { + method: (data) => { + let res = 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`(engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.mul(${args[i]}))` + return res + } + return false + }, + deterministic: true, + precise: true + }) + + engine.addMethod('/', { + method: (data) => { + let res = 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`(engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.div(${args[i]}))` + return res + } + return false + }, + deterministic: true, + precise: true + }) + + engine.addMethod('%', { + method: (data) => { + let res = 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`(engine.precision(${args[0]}))` + for (let i = 1; i < args.length; i++) res = buildState.compile`(${res}.mod(${args[i]}))` + return res + } + return false + }, + deterministic: true, + precise: 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..8777068 --- /dev/null +++ b/precision/scratch.js @@ -0,0 +1,13 @@ +import { LogicEngine } from '../index.js' +import { Decimal } from 'decimal.js' +import { configurePrecision } from './index.js' + +const ieee754Engine = new LogicEngine() +const decimalEngine = new LogicEngine() +configurePrecision(decimalEngine, Decimal.clone({ precision: 100 })) + +console.log(decimalEngine.build({ '+': ['85070591730234615847396907784232501249', 100] })().toFixed()) // 85070591730234615847396907784232501349 +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(decimalEngine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // false, because 0.1 + 0.2 = 0.3 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== From 41ec175f518eacc790b653a1797e09237e0f3b2c Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 19:14:45 -0600 Subject: [PATCH 03/15] Get rid of flags that did nothing --- defaultMethods.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 78bbf70..bc495c0 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -947,9 +947,6 @@ defaultMethods.var.compile = function (data, buildState) { // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods.var.optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true -// @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations -defaultMethods['<'].precise = defaultMethods['<='].precise = defaultMethods['>'].precise = defaultMethods['>='].precise = defaultMethods['=='].precise = defaultMethods['==='].precise = defaultMethods['!='].precise = defaultMethods['!=='].precise = true - export default { ...defaultMethods } From e3b45f5cdd5e7a7f503db98b05adfdd6dce65dad Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 19:39:50 -0600 Subject: [PATCH 04/15] Rewrite cat, simplify some things w/r/t the precision downgrade... I guess I don't need it --- defaultMethods.js | 32 +++++++++++++++++++------------- logic.js | 5 +---- precision/index.js | 12 ++++-------- precision/scratch.js | 8 ++++++++ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index bc495c0..78b45a2 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -520,11 +520,25 @@ 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: { + 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: { @@ -843,14 +857,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/logic.js b/logic.js index 0475c41..2051625 100644 --- a/logic.js +++ b/logic.js @@ -154,10 +154,7 @@ class LogicEngine { return res } - if (logic && typeof logic === 'object') { - if (this.precision && !options.precise && logic.toNumber) return Number(logic) - if (Object.keys(logic).length > 0) return this._parse(logic, data, above) - } + if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above) return logic } diff --git a/precision/index.js b/precision/index.js index e14605f..89bb3b0 100644 --- a/precision/index.js +++ b/precision/index.js @@ -17,8 +17,7 @@ export function configurePrecision (engine, constructor) { return false }, optimizeUnary: true, - deterministic: true, - precise: true + deterministic: true }) engine.addMethod('-', { @@ -38,8 +37,7 @@ export function configurePrecision (engine, constructor) { return false }, optimizeUnary: true, - deterministic: true, - precise: true + deterministic: true }) engine.addMethod('*', { @@ -74,8 +72,7 @@ export function configurePrecision (engine, constructor) { } return false }, - deterministic: true, - precise: true + deterministic: true }) engine.addMethod('%', { @@ -92,7 +89,6 @@ export function configurePrecision (engine, constructor) { } return false }, - deterministic: true, - precise: true + deterministic: true }) } diff --git a/precision/scratch.js b/precision/scratch.js index 8777068..e4979db 100644 --- a/precision/scratch.js +++ b/precision/scratch.js @@ -2,6 +2,14 @@ import { LogicEngine } from '../index.js' import { Decimal } from 'decimal.js' import { configurePrecision } from './index.js' +Decimal.prototype.toString = function () { + return this.toFixed() +} + +Decimal.prototype.valueOf = function () { + return this.toFixed() +} + const ieee754Engine = new LogicEngine() const decimalEngine = new LogicEngine() configurePrecision(decimalEngine, Decimal.clone({ precision: 100 })) From bd9bebc7ed8d228bc6681c839b57b495b0876ef4 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 20:51:28 -0600 Subject: [PATCH 05/15] Implement miscellaneous fixes and test a handful of setups for precision implementation --- compatibility.js | 2 ++ defaultMethods.js | 16 +++++++---- package.json | 3 +- precision/index.js | 71 +++++++++++++++++++++++++++++++++++----------- precision/test.js | 34 ++++++++++++++++++++++ 5 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 precision/test.js 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/defaultMethods.js b/defaultMethods.js index 78b45a2..ef4d460 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -224,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) @@ -262,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) @@ -310,6 +312,7 @@ const defaultMethods = { return 0 }, get: { + [Sync]: true, method: ([data, key, defaultValue], context, above, engine) => { const notFound = defaultValue === undefined ? null : defaultValue @@ -391,6 +394,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) => { @@ -411,7 +415,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 ( @@ -521,6 +525,7 @@ 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: { + [Sync]: true, method: (arr) => { if (typeof arr === 'string') return arr if (!Array.isArray(arr)) return arr.toString() @@ -659,8 +664,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 @@ -680,15 +685,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 } diff --git a/package.json b/package.json index bffdbde..f10ebcb 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ }, "jest": { "testPathIgnorePatterns": [ - "./bench" + "./bench", + "./precision" ] }, "exports": { diff --git a/precision/index.js b/precision/index.js index 89bb3b0..088f55e 100644 --- a/precision/index.js +++ b/precision/index.js @@ -1,5 +1,15 @@ -export function configurePrecision (engine, constructor) { + +export function configurePrecision (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 constructor(data) @@ -15,30 +25,28 @@ export function configurePrecision (engine, constructor) { return res } return false - }, - optimizeUnary: true, - deterministic: true - }) + } + }, { optimizeUnary: true, sync: true, deterministic: true }) engine.addMethod('-', { method: (data) => { if (typeof data === 'string') return constructor(data).mul(-1) if (typeof data === 'number') return constructor(data).mul(-1) let res = 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`(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 - }, - optimizeUnary: true, - deterministic: true - }) + } + }, { optimizeUnary: true, sync: true, deterministic: true }) engine.addMethod('*', { method: (data) => { @@ -53,10 +61,8 @@ export function configurePrecision (engine, constructor) { return res } return false - }, - deterministic: true, - precise: true - }) + } + }, { sync: true, deterministic: true }) engine.addMethod('/', { method: (data) => { @@ -71,9 +77,8 @@ export function configurePrecision (engine, constructor) { return res } return false - }, - deterministic: true - }) + } + }, { sync: true, deterministic: true }) engine.addMethod('%', { method: (data) => { @@ -88,7 +93,39 @@ export function configurePrecision (engine, constructor) { return res } return false + } + }, { 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]) + 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 + } + }, { 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]) + 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 }, deterministic: true - }) + }, { sync: true, deterministic: true }) } diff --git a/precision/test.js b/precision/test.js new file mode 100644 index 0000000..09e600f --- /dev/null +++ b/precision/test.js @@ -0,0 +1,34 @@ +import { AsyncLogicEngine } from '../index.js' +import { Decimal } from 'decimal.js' +import { configurePrecision } from './index.js' +import { isDeepStrictEqual } from 'util' + +import fs from 'fs' + +const tests = JSON.parse(fs.readFileSync('../bench/tests.json').toString()) + +Decimal.prototype.toString = function () { + return this.toFixed() +} + +Decimal.prototype.valueOf = function () { + return this.toFixed() +} + +const decimalEngine = new AsyncLogicEngine(undefined, { compatible: true }) +configurePrecision(decimalEngine, Decimal.clone({ precision: 100 })) + +let count = 0 +for (const test of tests) { + if (typeof test !== 'string') { + let result = await decimalEngine.run(test[0], test[1]) + if (result && result.toNumber) result = Number(result) + if (Array.isArray(result)) result = result.map((x) => (x && x.toNumber ? Number(x) : x)) + if (!isDeepStrictEqual(result, test[2])) { + count++ + console.log(test[0], test[2], result) + } + } +} + +console.log(count, 'Wrong') From eb05083f8aceb08fe721cb8c2d391386caaeae91 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Thu, 26 Dec 2024 23:15:24 -0600 Subject: [PATCH 06/15] Tweak tests and precision accuracy --- precision/index.js | 116 +++++++++++++++++++++++++++++++++++-------- precision/scratch.js | 9 ++-- precision/test.js | 15 +++++- 3 files changed, 113 insertions(+), 27 deletions(-) diff --git a/precision/index.js b/precision/index.js index 088f55e..4c6ca12 100644 --- a/precision/index.js +++ b/precision/index.js @@ -12,88 +12,93 @@ export function configurePrecision (engine, constructor, compatible = true) { engine.addMethod('+', { method: (data) => { - if (typeof data === 'string') return constructor(data) - if (typeof data === 'number') return constructor(data) - let res = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + 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 constructor(data).mul(-1) - if (typeof data === 'number') return constructor(data).mul(-1) - let res = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + 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 = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + 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 = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + 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 = constructor(data[0]) + 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`(engine.precision(${args[0]}))` + 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('===', { @@ -109,7 +114,8 @@ export function configurePrecision (engine, constructor, compatible = true) { if (args[i - 1] !== args[i]) return false } return true - } + }, + traverse: true }, { sync: true, deterministic: true }) engine.addMethod('!==', { @@ -126,6 +132,74 @@ export function configurePrecision (engine, constructor, compatible = true) { } return false }, - deterministic: true + 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 }) } diff --git a/precision/scratch.js b/precision/scratch.js index e4979db..bc58a92 100644 --- a/precision/scratch.js +++ b/precision/scratch.js @@ -6,15 +6,14 @@ Decimal.prototype.toString = function () { return this.toFixed() } -Decimal.prototype.valueOf = function () { - return this.toFixed() -} - const ieee754Engine = new LogicEngine() const decimalEngine = new LogicEngine() configurePrecision(decimalEngine, Decimal.clone({ precision: 100 })) -console.log(decimalEngine.build({ '+': ['85070591730234615847396907784232501249', 100] })().toFixed()) // 85070591730234615847396907784232501349 +console.log(ieee754Engine.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(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 diff --git a/precision/test.js b/precision/test.js index 09e600f..858a9f2 100644 --- a/precision/test.js +++ b/precision/test.js @@ -1,4 +1,4 @@ -import { AsyncLogicEngine } from '../index.js' +import { AsyncLogicEngine, LogicEngine } from '../index.js' import { Decimal } from 'decimal.js' import { configurePrecision } from './index.js' import { isDeepStrictEqual } from 'util' @@ -32,3 +32,16 @@ for (const test of tests) { } console.log(count, 'Wrong') + + +const decimalEngineSync = new LogicEngine(undefined, { compatible: true }) +configurePrecision(decimalEngineSync, Decimal.clone({ precision: 100 })) + +console.time('built') +for (const test of tests) { + const f = decimalEngineSync.build(test[0]) + for (let i = 0; i < 1e5; i++) { + f(test[1]) + } +} +console.timeEnd('built') \ No newline at end of file From 7a74c0d3784da6273babe1568142e37e75d98f73 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 00:26:01 -0600 Subject: [PATCH 07/15] Add decimal precision to tests --- compatible.test.js | 179 ++++++++++++--------------------------------- package.json | 1 + precision/test.js | 47 ------------ yarn.lock | 5 ++ 4 files changed, 54 insertions(+), 178 deletions(-) delete mode 100644 precision/test.js diff --git a/compatible.test.js b/compatible.test.js index 338da5b..118afc9 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,55 @@ 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' + } + 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/package.json b/package.json index f10ebcb..f52b1bf 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "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" }, "devDependencies": { + "decimal.js": "^10.4.3", "coveralls": "^3.1.1", "cross-env": "^7.0.3", "eslint": "^7.32.0", diff --git a/precision/test.js b/precision/test.js deleted file mode 100644 index 858a9f2..0000000 --- a/precision/test.js +++ /dev/null @@ -1,47 +0,0 @@ -import { AsyncLogicEngine, LogicEngine } from '../index.js' -import { Decimal } from 'decimal.js' -import { configurePrecision } from './index.js' -import { isDeepStrictEqual } from 'util' - -import fs from 'fs' - -const tests = JSON.parse(fs.readFileSync('../bench/tests.json').toString()) - -Decimal.prototype.toString = function () { - return this.toFixed() -} - -Decimal.prototype.valueOf = function () { - return this.toFixed() -} - -const decimalEngine = new AsyncLogicEngine(undefined, { compatible: true }) -configurePrecision(decimalEngine, Decimal.clone({ precision: 100 })) - -let count = 0 -for (const test of tests) { - if (typeof test !== 'string') { - let result = await decimalEngine.run(test[0], test[1]) - if (result && result.toNumber) result = Number(result) - if (Array.isArray(result)) result = result.map((x) => (x && x.toNumber ? Number(x) : x)) - if (!isDeepStrictEqual(result, test[2])) { - count++ - console.log(test[0], test[2], result) - } - } -} - -console.log(count, 'Wrong') - - -const decimalEngineSync = new LogicEngine(undefined, { compatible: true }) -configurePrecision(decimalEngineSync, Decimal.clone({ precision: 100 })) - -console.time('built') -for (const test of tests) { - const f = decimalEngineSync.build(test[0]) - for (let i = 0; i < 1e5; i++) { - f(test[1]) - } -} -console.timeEnd('built') \ No newline at end of file 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" From 62b4f4e778fdcfc0db37bca2df1018fff001f188 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 00:36:59 -0600 Subject: [PATCH 08/15] Add `configurePrecision` to the library --- index.js | 4 +++- precision/index.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) 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/precision/index.js b/precision/index.js index 4c6ca12..9ee7180 100644 --- a/precision/index.js +++ b/precision/index.js @@ -1,4 +1,34 @@ +/** + * Allows you to configure precision for JSON Logic Engine. + * + * Essentially, pass the constructor for `decimal.js` in as the second argument. + * + * @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` + * + * @param {import('../logic.d.ts').default | import('../asyncLogic.d.ts').default} engine + * @param {*} constructor + * @param {Boolean} compatible + */ export function configurePrecision (engine, constructor, compatible = true) { engine.precision = constructor From b728b89cac52c4fcfa5883a65d3236056ce1140f Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 00:47:19 -0600 Subject: [PATCH 09/15] Added some comments to `configurePrecision` --- package.json | 4 ++-- precision/index.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f52b1bf..b277f62 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "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", diff --git a/precision/index.js b/precision/index.js index 9ee7180..6b4f6b9 100644 --- a/precision/index.js +++ b/precision/index.js @@ -25,6 +25,13 @@ * - `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 {*} constructor * @param {Boolean} compatible From 1c8854dde5766b2b8abc59b3d1802dfba75072d1 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 01:33:22 -0600 Subject: [PATCH 10/15] Improve precision compatibility further (for when JSON is parsed with Decimal reviver rather than number) --- defaultMethods.js | 2 ++ precision/index.js | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index ef4d460..8db33a4 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -300,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) diff --git a/precision/index.js b/precision/index.js index 6b4f6b9..20e224a 100644 --- a/precision/index.js +++ b/precision/index.js @@ -141,8 +141,7 @@ export function configurePrecision (engine, constructor, compatible = 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]) + if (args[0].eq && args[1].eq) return args[0].eq(args[1]) return args[0] === args[1] } for (let i = 1; i < args.length; i++) { @@ -155,11 +154,48 @@ export function configurePrecision (engine, constructor, compatible = true) { traverse: true }, { sync: true, deterministic: true }) - engine.addMethod('!==', { + 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) return !args[0].eq(args[1]) return args[0] !== args[1] } for (let i = 1; i < args.length; i++) { From 5feb258ea544a0263fd47dba233afe0a5202077a Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 01:36:54 -0600 Subject: [PATCH 11/15] Strict Equality Numeric Promotion --- precision/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/precision/index.js b/precision/index.js index 20e224a..03f8d29 100644 --- a/precision/index.js +++ b/precision/index.js @@ -141,7 +141,8 @@ export function configurePrecision (engine, constructor, compatible = true) { engine.addMethod('===', { method: (args) => { if (args.length === 2) { - if (args[0].eq && args[1].eq) return args[0].eq(args[1]) + 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++) { @@ -195,7 +196,8 @@ export function configurePrecision (engine, constructor, compatible = true) { engine.addMethod('!==', { method: (args) => { if (args.length === 2) { - if (args[0].eq && args[1].eq) return !args[0].eq(args[1]) + 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++) { From 31cbb88a3a3df5ed06a785e07e8e27e6ea45a862 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 09:56:56 -0600 Subject: [PATCH 12/15] Exclude precision from npmignore --- .npmignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 6942255b86549925e6faf00cce4196e0b396f69d Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 17:08:38 -0600 Subject: [PATCH 13/15] Add another precision mode that is zero dep but improves accuracy for general use. --- compatible.test.js | 7 ++ precision/index.js | 158 ++++++++++++++++++++++++++++++++----------- precision/scratch.js | 13 +++- 3 files changed, 138 insertions(+), 40 deletions(-) diff --git a/compatible.test.js b/compatible.test.js index 118afc9..c2f221f 100644 --- a/compatible.test.js +++ b/compatible.test.js @@ -40,6 +40,13 @@ for (let i = 0; i < 16; i++) { 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]) } diff --git a/precision/index.js b/precision/index.js index 03f8d29..4f7e06d 100644 --- a/precision/index.js +++ b/precision/index.js @@ -1,42 +1,6 @@ +import defaultMethods from '../defaultMethods.js' -/** - * Allows you to configure precision for JSON Logic Engine. - * - * Essentially, pass the constructor for `decimal.js` in as the second argument. - * - * @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 {*} constructor - * @param {Boolean} compatible - */ -export function configurePrecision (engine, constructor, compatible = true) { +function configurePrecisionDecimalJs (engine, constructor, compatible = true) { engine.precision = constructor engine.truthy = (data) => { @@ -278,3 +242,121 @@ export function configurePrecision (engine, constructor, compatible = 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(data[i])) res += item + else { + res += item | 0 + overflow += (item - (item | 0)) * 1e6 + } + } + return res + (overflow / 1e6) + }, { deterministic: true, sync: 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] + 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(data[i])) res -= item + else { + res -= item | 0 + overflow += (item - (item | 0)) * 1e6 + } + } + return res - (overflow / 1e6) + }, { deterministic: true, sync: 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/scratch.js b/precision/scratch.js index bc58a92..21ac512 100644 --- a/precision/scratch.js +++ b/precision/scratch.js @@ -5,16 +5,25 @@ 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.clone({ precision: 100 })) + +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 From 4e90c505c5c350d1206a686b6558a4b83c543e24 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 17:43:12 -0600 Subject: [PATCH 14/15] Additional precision tweaks... --- precision/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/precision/index.js b/precision/index.js index 4f7e06d..54f9fc7 100644 --- a/precision/index.js +++ b/precision/index.js @@ -303,7 +303,7 @@ export function configurePrecision (engine, constructor, compatible = true) { if (Number.isInteger(data[i])) res += item else { res += item | 0 - overflow += (item - (item | 0)) * 1e6 + overflow += +('0.' + item.toString().split('.')[1]) * 1e6 } } return res + (overflow / 1e6) @@ -315,6 +315,12 @@ export function configurePrecision (engine, constructor, compatible = true) { 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 } @@ -341,7 +347,7 @@ export function configurePrecision (engine, constructor, compatible = true) { if (Number.isInteger(data[i])) res -= item else { res -= item | 0 - overflow += (item - (item | 0)) * 1e6 + overflow += +('0.' + item.toString().split('.')[1]) * 1e6 } } return res - (overflow / 1e6) From 272d4ba90c13a265821f78072d7d186bbb91ee19 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Fri, 27 Dec 2024 17:49:33 -0600 Subject: [PATCH 15/15] Add `+` unary fix --- precision/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/precision/index.js b/precision/index.js index 54f9fc7..a6a1606 100644 --- a/precision/index.js +++ b/precision/index.js @@ -300,14 +300,15 @@ export function configurePrecision (engine, constructor, compatible = true) { let overflow = 0 for (let i = 0; i < data.length; i++) { const item = +data[i] - if (Number.isInteger(data[i])) res += item + 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 }) + }, { deterministic: true, sync: true, optimizeUnary: true }) engine.addMethod('*', (data) => { const SCALE_FACTOR = 1e6 // Fixed scale for precision @@ -344,14 +345,14 @@ export function configurePrecision (engine, constructor, compatible = true) { let overflow = 0 for (let i = 1; i < data.length; i++) { const item = +data[i] - if (Number.isInteger(data[i])) res -= item + 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 }) + }, { deterministic: true, sync: true, optimizeUnary: true }) engine.addMethod('%', (data) => { let res = data[0]