Skip to content

Commit 6942255

Browse files
committed
Add another precision mode that is zero dep but improves accuracy for general use.
1 parent 31cbb88 commit 6942255

File tree

3 files changed

+138
-40
lines changed

3 files changed

+138
-40
lines changed

compatible.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ for (let i = 0; i < 16; i++) {
4040
if (i & 8) {
4141
configurePrecision(engine, Decimal)
4242
res += ' decimal'
43+
44+
// Copy in another decimal engine
45+
const preciseEngine = (i & 1) ? new AsyncLogicEngine(undefined, { compatible: true }) : new LogicEngine(undefined, { compatible: true })
46+
preciseEngine.disableInline = engine.disableInline
47+
preciseEngine.disableInterpretedOptimization = engine.disableInterpretedOptimization
48+
configurePrecision(preciseEngine, 'precise')
49+
engines.push([preciseEngine, res + ' improved'])
4350
}
4451
engines.push([engine, res])
4552
}

precision/index.js

+120-38
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,6 @@
1+
import defaultMethods from '../defaultMethods.js'
12

2-
/**
3-
* Allows you to configure precision for JSON Logic Engine.
4-
*
5-
* Essentially, pass the constructor for `decimal.js` in as the second argument.
6-
*
7-
* @example ```js
8-
* import { LogicEngine, configurePrecision } from 'json-logic-js'
9-
* import { Decimal } from 'decimal.js' // or decimal.js-light
10-
*
11-
* const engine = new LogicEngine()
12-
* configurePrecision(engine, Decimal)
13-
* ```
14-
*
15-
* The class this mechanism uses requires the following methods to be implemented:
16-
* - `eq`
17-
* - `gt`
18-
* - `gte`
19-
* - `lt`
20-
* - `lte`
21-
* - `plus`
22-
* - `minus`
23-
* - `mul`
24-
* - `div`
25-
* - `mod`
26-
* - `toNumber`
27-
*
28-
* ### FAQ:
29-
*
30-
* Q: Why is this not included in the class?
31-
*
32-
* A: This mechanism reimplements a handful of operators. Keeping this method separate makes it possible to tree-shake this code out
33-
* if you don't need it.
34-
*
35-
* @param {import('../logic.d.ts').default | import('../asyncLogic.d.ts').default} engine
36-
* @param {*} constructor
37-
* @param {Boolean} compatible
38-
*/
39-
export function configurePrecision (engine, constructor, compatible = true) {
3+
function configurePrecisionDecimalJs (engine, constructor, compatible = true) {
404
engine.precision = constructor
415

426
engine.truthy = (data) => {
@@ -278,3 +242,121 @@ export function configurePrecision (engine, constructor, compatible = true) {
278242
traverse: true
279243
}, { sync: true, deterministic: true })
280244
}
245+
246+
/**
247+
* Allows you to configure precision for JSON Logic Engine.
248+
*
249+
* You can pass the following in:
250+
* - `ieee754` - Uses the IEEE 754 standard for calculations.
251+
* - `precise` - Tries to improve accuracy of calculations by scaling numbers during operations.
252+
* - A constructor for decimal.js.
253+
*
254+
* @example ```js
255+
* import { LogicEngine, configurePrecision } from 'json-logic-js'
256+
* import { Decimal } from 'decimal.js' // or decimal.js-light
257+
*
258+
* const engine = new LogicEngine()
259+
* configurePrecision(engine, Decimal)
260+
* ```
261+
*
262+
* The class this mechanism uses requires the following methods to be implemented:
263+
* - `eq`
264+
* - `gt`
265+
* - `gte`
266+
* - `lt`
267+
* - `lte`
268+
* - `plus`
269+
* - `minus`
270+
* - `mul`
271+
* - `div`
272+
* - `mod`
273+
* - `toNumber`
274+
*
275+
* ### FAQ:
276+
*
277+
* Q: Why is this not included in the class?
278+
*
279+
* A: This mechanism reimplements a handful of operators. Keeping this method separate makes it possible to tree-shake this code out
280+
* if you don't need it.
281+
*
282+
* @param {import('../logic.d.ts').default | import('../asyncLogic.d.ts').default} engine
283+
* @param {'precise' | 'ieee754' | (...args: any[]) => any} constructor
284+
* @param {Boolean} compatible
285+
*/
286+
export function configurePrecision (engine, constructor, compatible = true) {
287+
if (typeof constructor === 'function') return configurePrecisionDecimalJs(engine, constructor, compatible)
288+
289+
if (constructor === 'ieee754') {
290+
const operators = ['+', '-', '*', '/', '%', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
291+
for (const operator of operators) engine.methods[operator] = defaultMethods[operator]
292+
}
293+
294+
if (constructor !== 'precise') throw new Error('Unsupported precision type')
295+
296+
engine.addMethod('+', (data) => {
297+
if (typeof data === 'string') return +data
298+
if (typeof data === 'number') return +data
299+
let res = 0
300+
let overflow = 0
301+
for (let i = 0; i < data.length; i++) {
302+
const item = +data[i]
303+
if (Number.isInteger(data[i])) res += item
304+
else {
305+
res += item | 0
306+
overflow += (item - (item | 0)) * 1e6
307+
}
308+
}
309+
return res + (overflow / 1e6)
310+
}, { deterministic: true, sync: true })
311+
312+
engine.addMethod('*', (data) => {
313+
const SCALE_FACTOR = 1e6 // Fixed scale for precision
314+
let result = 1
315+
316+
for (let i = 0; i < data.length; i++) {
317+
const item = +data[i]
318+
result *= (item * SCALE_FACTOR) | 0
319+
result /= SCALE_FACTOR
320+
}
321+
322+
return result
323+
}, { deterministic: true, sync: true })
324+
325+
engine.addMethod('/', (data) => {
326+
let res = data[0]
327+
for (let i = 1; i < data.length; i++) res /= +data[i]
328+
// if the value is really close to 0, we'll just return 0
329+
if (Math.abs(res) < 1e-10) return 0
330+
return res
331+
}, { deterministic: true, sync: true })
332+
333+
engine.addMethod('-', (data) => {
334+
if (typeof data === 'string') return -data
335+
if (typeof data === 'number') return -data
336+
if (data.length === 1) return -data[0]
337+
let res = data[0]
338+
let overflow = 0
339+
for (let i = 1; i < data.length; i++) {
340+
const item = +data[i]
341+
if (Number.isInteger(data[i])) res -= item
342+
else {
343+
res -= item | 0
344+
overflow += (item - (item | 0)) * 1e6
345+
}
346+
}
347+
return res - (overflow / 1e6)
348+
}, { deterministic: true, sync: true })
349+
350+
engine.addMethod('%', (data) => {
351+
let res = data[0]
352+
353+
if (data.length === 2) {
354+
if (data[0] < 1e6 && data[1] < 1e6) return ((data[0] * 10e3) % (data[1] * 10e3)) / 10e3
355+
}
356+
357+
for (let i = 1; i < data.length; i++) res %= +data[i]
358+
// if the value is really close to 0, we'll just return 0
359+
if (Math.abs(res) < 1e-10) return 0
360+
return res
361+
}, { deterministic: true, sync: true })
362+
}

precision/scratch.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@ import { configurePrecision } from './index.js'
55
Decimal.prototype.toString = function () {
66
return this.toFixed()
77
}
8-
98
const ieee754Engine = new LogicEngine()
9+
const improvedEngine = new LogicEngine()
1010
const decimalEngine = new LogicEngine()
11-
configurePrecision(decimalEngine, Decimal.clone({ precision: 100 }))
11+
12+
configurePrecision(decimalEngine, Decimal)
13+
configurePrecision(improvedEngine, 'precise')
1214

1315
console.log(ieee754Engine.build({ '*': [9007199254740991, 5] })()) // 45035996273704950, inaccurate
16+
console.log(improvedEngine.build({ '*': [9007199254740991, 5] })()) // 45035996273704950, inaccurate
1417
console.log(decimalEngine.build({ '*': [9007199254740991, 5] })()) // 45035996273704955, accurate
1518

1619
console.log(ieee754Engine.run({ '+': [0.1, 0.2] })) // 0.30000000000000004
20+
console.log(improvedEngine.run({ '+': [0.1, 0.2] })) // 0.3
1721
console.log(decimalEngine.run({ '+': [0.1, 0.2] })) // 0.3
1822

1923
console.log(ieee754Engine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // true, because 0.1 + 0.2 = 0.30000000000000004
24+
console.log(improvedEngine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // false, because 0.1 + 0.2 = 0.3
2025
console.log(decimalEngine.run({ '>': [{ '+': [0.1, 0.2] }, 0.3] })) // false, because 0.1 + 0.2 = 0.3
26+
27+
console.log(ieee754Engine.run({ '%': [0.0075, 0.0001] })) // 0.00009999999999999937
28+
console.log(improvedEngine.run({ '%': [0.0075, 0.0001] })) // 0
29+
console.log(decimalEngine.run({ '%': [0.0075, 0.0001] })) // 0

0 commit comments

Comments
 (0)