From badbedbd4e9727bc7387cf712bd45c5f6660b221 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 21 Mar 2022 07:43:27 -0700 Subject: [PATCH] Refactor/simplify core cleanup (#2490) * refactor: don't simplify constants in simplifyCore Keeps the operation of simplifyCore cleanly separate from simplifyConstant. * fix; handle multiple consecutive operations in simplifyCore() Also adds support for logical operators. Resolves #2484. * feat: export simplifyConstant Now that simplifyCore does not do any constant folding, clients may wish to access that behavior via simplifyConstant. Moreover, exporting it makes it easier to use in custom rule lists for simplify(). Also adds docs, embedded docs, and tests for simplifyConstant(). Also fixes simplifyCore() on logical functions (they always return boolean, rather than "short-circuiting"). Resolves #2459. --- src/expression/embeddedDocs/embeddedDocs.js | 2 + .../function/algebra/simplifyConstant.js | 16 ++ .../function/algebra/simplifyCore.js | 2 +- src/factoriesAny.js | 1 + src/factoriesNumber.js | 1 + src/function/algebra/rationalize.js | 20 +- src/function/algebra/simplify.js | 20 +- .../{simplify => }/simplifyConstant.js | 58 +++++- src/function/algebra/simplifyCore.js | 176 ++++++++++-------- .../function/algebra/simplify.test.js | 22 ++- .../function/algebra/simplifyConstant.test.js | 38 ++++ .../function/algebra/simplifyCore.test.js | 30 ++- types/index.d.ts | 8 +- types/index.ts | 6 + 14 files changed, 261 insertions(+), 139 deletions(-) create mode 100644 src/expression/embeddedDocs/function/algebra/simplifyConstant.js rename src/function/algebra/{simplify => }/simplifyConstant.js (89%) create mode 100644 test/unit-tests/function/algebra/simplifyConstant.test.js diff --git a/src/expression/embeddedDocs/embeddedDocs.js b/src/expression/embeddedDocs/embeddedDocs.js index 960978340f..2f7ce32562 100644 --- a/src/expression/embeddedDocs/embeddedDocs.js +++ b/src/expression/embeddedDocs/embeddedDocs.js @@ -40,6 +40,7 @@ import { qrDocs } from './function/algebra/qr.js' import { rationalizeDocs } from './function/algebra/rationalize.js' import { resolveDocs } from './function/algebra/resolve.js' import { simplifyDocs } from './function/algebra/simplify.js' +import { simplifyConstantDocs } from './function/algebra/simplifyConstant.js' import { simplifyCoreDocs } from './function/algebra/simplifyCore.js' import { sluDocs } from './function/algebra/slu.js' import { symbolicEqualDocs } from './function/algebra/symbolicEqual.js' @@ -334,6 +335,7 @@ export const embeddedDocs = { leafCount: leafCountDocs, resolve: resolveDocs, simplify: simplifyDocs, + simplifyConstant: simplifyConstantDocs, simplifyCore: simplifyCoreDocs, symbolicEqual: symbolicEqualDocs, rationalize: rationalizeDocs, diff --git a/src/expression/embeddedDocs/function/algebra/simplifyConstant.js b/src/expression/embeddedDocs/function/algebra/simplifyConstant.js new file mode 100644 index 0000000000..f86645f969 --- /dev/null +++ b/src/expression/embeddedDocs/function/algebra/simplifyConstant.js @@ -0,0 +1,16 @@ +export const simplifyConstantDocs = { + name: 'simplifyConstant', + category: 'Algebra', + syntax: [ + 'simplifyConstant(expr)', + 'simplifyConstant(expr, options)' + ], + description: 'Replace constant subexpressions of node with their values.', + examples: [ + 'simplifyConatant("(3-3)*x")', + 'simplifyConstant(parse("z-cos(tau/8)"))' + ], + seealso: [ + 'simplify', 'simplifyCore', 'evaluate' + ] +} diff --git a/src/expression/embeddedDocs/function/algebra/simplifyCore.js b/src/expression/embeddedDocs/function/algebra/simplifyCore.js index 6d967847f7..9a12202675 100644 --- a/src/expression/embeddedDocs/function/algebra/simplifyCore.js +++ b/src/expression/embeddedDocs/function/algebra/simplifyCore.js @@ -10,6 +10,6 @@ export const simplifyCoreDocs = { 'simplifyCore(parse("(x+0)*2"))' ], seealso: [ - 'simplify', 'evaluate' + 'simplify', 'simplifyConstant', 'evaluate' ] } diff --git a/src/factoriesAny.js b/src/factoriesAny.js index cd50841edf..822739ec03 100644 --- a/src/factoriesAny.js +++ b/src/factoriesAny.js @@ -244,6 +244,7 @@ export { createCatalan } from './function/combinatorics/catalan.js' export { createComposition } from './function/combinatorics/composition.js' export { createLeafCount } from './function/algebra/leafCount.js' export { createSimplify } from './function/algebra/simplify.js' +export { createSimplifyConstant } from './function/algebra/simplifyConstant.js' export { createSimplifyCore } from './function/algebra/simplifyCore.js' export { createResolve } from './function/algebra/resolve.js' export { createSymbolicEqual } from './function/algebra/symbolicEqual.js' diff --git a/src/factoriesNumber.js b/src/factoriesNumber.js index 23678ffc3d..94a7f89058 100644 --- a/src/factoriesNumber.js +++ b/src/factoriesNumber.js @@ -91,6 +91,7 @@ export { createChain } from './type/chain/function/chain.js' // algebra export { createResolve } from './function/algebra/resolve.js' export { createSimplify } from './function/algebra/simplify.js' +export { createSimplifyConstant } from './function/algebra/simplifyConstant.js' export { createSimplifyCore } from './function/algebra/simplifyCore.js' export { createDerivative } from './function/algebra/derivative.js' export { createRationalize } from './function/algebra/rationalize.js' diff --git a/src/function/algebra/rationalize.js b/src/function/algebra/rationalize.js index 3a02024887..e15457376c 100644 --- a/src/function/algebra/rationalize.js +++ b/src/function/algebra/rationalize.js @@ -1,6 +1,5 @@ import { isInteger } from '../../utils/number.js' import { factory } from '../../utils/factory.js' -import { createSimplifyConstant } from './simplify/simplifyConstant.js' const name = 'rationalize' const dependencies = [ @@ -14,6 +13,7 @@ const dependencies = [ 'divide', 'pow', 'parse', + 'simplifyConstant', 'simplifyCore', 'simplify', '?bignumber', @@ -42,6 +42,7 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({ divide, pow, parse, + simplifyConstant, simplifyCore, simplify, fraction, @@ -58,23 +59,6 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({ SymbolNode, ParenthesisNode }) => { - const simplifyConstant = createSimplifyConstant({ - typed, - config, - mathWithTransform, - matrix, - fraction, - bignumber, - AccessorNode, - ArrayNode, - ConstantNode, - FunctionNode, - IndexNode, - ObjectNode, - OperatorNode, - SymbolNode - }) - /** * Transform a rationalizable expression in a rational fraction. * If rational fraction is one variable polynomial then converts diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index 0bac52c155..2a60c12b14 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -1,7 +1,6 @@ import { isConstantNode, isParenthesisNode } from '../../utils/is.js' import { factory } from '../../utils/factory.js' import { createUtil } from './simplify/util.js' -import { createSimplifyConstant } from './simplify/simplifyConstant.js' import { hasOwnProperty } from '../../utils/object.js' import { createEmptyMap, createMap } from '../../utils/map.js' @@ -18,6 +17,7 @@ const dependencies = [ 'isZero', 'equal', 'resolve', + 'simplifyConstant', 'simplifyCore', '?fraction', '?bignumber', @@ -47,6 +47,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( isZero, equal, resolve, + simplifyConstant, simplifyCore, fraction, bignumber, @@ -63,23 +64,6 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( SymbolNode } ) => { - const simplifyConstant = createSimplifyConstant({ - typed, - config, - mathWithTransform, - matrix, - fraction, - bignumber, - AccessorNode, - ArrayNode, - ConstantNode, - FunctionNode, - IndexNode, - ObjectNode, - OperatorNode, - SymbolNode - }) - const { hasProperty, isCommutative, isAssociative, mergeContext, flatten, unflattenr, unflattenl, createMakeNodeFunction, defaultContext, realContext, positiveContext } = createUtil({ FunctionNode, OperatorNode, SymbolNode }) diff --git a/src/function/algebra/simplify/simplifyConstant.js b/src/function/algebra/simplifyConstant.js similarity index 89% rename from src/function/algebra/simplify/simplifyConstant.js rename to src/function/algebra/simplifyConstant.js index 577b0f8c32..6c50002473 100644 --- a/src/function/algebra/simplify/simplifyConstant.js +++ b/src/function/algebra/simplifyConstant.js @@ -1,12 +1,12 @@ -// TODO this could be improved by simplifying seperated constants under associative and commutative operators -import { isFraction, isMatrix, isNode, isArrayNode, isConstantNode, isIndexNode, isObjectNode, isOperatorNode } from '../../../utils/is.js' -import { factory } from '../../../utils/factory.js' -import { createUtil } from './util.js' -import { noBignumber, noFraction } from '../../../utils/noop.js' +import { isFraction, isMatrix, isNode, isArrayNode, isConstantNode, isIndexNode, isObjectNode, isOperatorNode } from '../../utils/is.js' +import { factory } from '../../utils/factory.js' +import { createUtil } from './simplify/util.js' +import { noBignumber, noFraction } from '../../utils/noop.js' const name = 'simplifyConstant' const dependencies = [ 'typed', + 'parse', 'config', 'mathWithTransform', 'matrix', @@ -24,6 +24,7 @@ const dependencies = [ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies, ({ typed, + parse, config, mathWithTransform, matrix, @@ -41,9 +42,50 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies const { isCommutative, isAssociative, allChildren, createMakeNodeFunction } = createUtil({ FunctionNode, OperatorNode, SymbolNode }) - function simplifyConstant (expr, options) { - return _ensureNode(foldFraction(expr, options)) - } + /** + * simplifyConstant() takes a mathjs expression (either a Node representing + * a parse tree or a string which it parses to produce a node), and replaces + * any subexpression of it consisting entirely of constants with the computed + * value of that subexpression. + * + * Syntax: + * + * simplifyConstant(expr) + * simplifyConstant(expr, options) + * + * Examples: + * + * math.simplifyConstant('x + 4*3/6') // Node "x + 2" + * math.simplifyConstant('z cos(0)') // Node "z 1" + * math.simplifyConstant('(5.2 + 1.08)t', {exactFractions: false}) // Node "6.28 t" + * + * See also: + * + * simplify, simplifyCore, resolve, derivative + * + * @param {Node | string} node + * The expression to be simplified + * @param {Object} options + * Simplification options, as per simplify() + * @return {Node} Returns expression with constant subexpressions evaluated + */ + const simplifyConstant = typed('simplifyConstant', { + string: function (expr) { + return this(parse(expr), {}) + }, + + 'string, Object': function (expr, options) { + return this(parse(expr), options) + }, + + Node: function (node) { + return this(node, {}) + }, + + 'Node, Object': function (expr, options) { + return _ensureNode(foldFraction(expr, options)) + } + }) function _removeFractions (thing) { if (isFraction(thing)) { diff --git a/src/function/algebra/simplifyCore.js b/src/function/algebra/simplifyCore.js index e364dc8333..b9f79b1764 100644 --- a/src/function/algebra/simplifyCore.js +++ b/src/function/algebra/simplifyCore.js @@ -47,14 +47,23 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ }) => { const node0 = new ConstantNode(0) const node1 = new ConstantNode(1) + const nodeT = new ConstantNode(true) + const nodeF = new ConstantNode(false) + // test if a node will always have a boolean value (true/false) + // not sure if this list is complete + function isAlwaysBoolean (node) { + return isOperatorNode(node) && ['and', 'not', 'or'].includes(node.op) + } const { hasProperty, isCommutative } = createUtil({ FunctionNode, OperatorNode, SymbolNode }) /** * simplifyCore() performs single pass simplification suitable for - * applications requiring ultimate performance. In contrast, simplify() - * extends simplifyCore() with additional passes to provide deeper - * simplification. + * applications requiring ultimate performance. To roughly summarize, + * it handles cases along the lines of simplifyConstant() but where + * knowledge of a single argument is sufficient to determine the value. + * In contrast, simplify() extends simplifyCore() with additional passes + * to provide deeper simplification (such as gathering like terms). * * Specifically, simplifyCore: * @@ -62,14 +71,14 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ * operator forms. * * Removes operators or function calls that are guaranteed to have no * effect (such as unary '+'). - * * Removes double unary '-' + * * Removes double unary '-', '~', and 'not' * * Eliminates addition/subtraction of 0 and multiplication/division/powers * by 1 or 0. * * Converts addition of a negation into subtraction. + * * Eliminates logical operations with constant true or false leading + * arguments. * * Puts constants on the left of a product, if multiplication is * considered commutative by the options (which is the default) - * * Replaces subexpressions that consist of basic arithmetic operations on - * constants with their values. * * Syntax: * @@ -78,15 +87,15 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ * * Examples: * - * const f = math.parse('2 * 1 * x ^ (2 - 1)') + * const f = math.parse('2 * 1 * x ^ (1 - 0)') * math.simplifyCore(f) // Node "2 * x" - * math.simplify('2 * 1 * x ^ (2 - 1)', [math.simplifyCore]) // Node "2 * x" + * math.simplify('2 * 1 * x ^ (1 - 0)', [math.simplifyCore]) // Node "2 * x" * * See also: * - * simplify, resolve, derivative + * simplify, simplifyConstant, resolve, derivative * - * @param {Node} node + * @param {Node | string} node * The expression to be simplified * @param {Object} options * Simplification options, as per simplify() @@ -148,62 +157,70 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ if (isOperatorNode(node) && node.isUnary()) { const a0 = simplifyCore(node.args[0], options) + if (node.op === '~') { // bitwise not + if (isOperatorNode(a0) && a0.isUnary() && a0.op === '~') { + return a0.args[0] + } + } + if (node.op === 'not') { // logical not + if (isOperatorNode(a0) && a0.isUnary() && a0.op === 'not') { + // Has the effect of turning the argument into a boolean + // So can only eliminate the double negation if + // the inside is already boolean + if (isAlwaysBoolean(a0.args[0])) { + return a0.args[0] + } + } + } + let finish = true if (node.op === '-') { // unary minus if (isOperatorNode(a0)) { + if (a0.isBinary() && a0.fn === 'subtract') { + node = new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]) + finish = false // continue to process the new binary node + } if (a0.isUnary() && a0.op === '-') { return a0.args[0] - } else if (a0.isBinary() && a0.fn === 'subtract') { - return new OperatorNode('-', 'subtract', [a0.args[1], a0.args[0]]) } } - return new OperatorNode(node.op, node.fn, [a0]) } - } else if (isOperatorNode(node) && node.isBinary()) { + if (finish) return new OperatorNode(node.op, node.fn, [a0]) + } + if (isOperatorNode(node) && node.isBinary()) { const a0 = simplifyCore(node.args[0], options) - const a1 = simplifyCore(node.args[1], options) + let a1 = simplifyCore(node.args[1], options) if (node.op === '+') { - if (isConstantNode(a0)) { - if (isZero(a0.value)) { - return a1 - } else if (isConstantNode(a1)) { - return new ConstantNode(add(a0.value, a1.value)) - } + if (isConstantNode(a0) && isZero(a0.value)) { + return a1 } if (isConstantNode(a1) && isZero(a1.value)) { return a0 } if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { - return new OperatorNode('-', 'subtract', [a0, a1.args[0]]) + a1 = a1.args[0] + node = new OperatorNode('-', 'subtract', [a0, a1]) } - return new OperatorNode(node.op, node.fn, a1 ? [a0, a1] : [a0]) - } else if (node.op === '-') { - if (isConstantNode(a0) && a1) { - if (isConstantNode(a1)) { - return new ConstantNode(subtract(a0.value, a1.value)) - } else if (isZero(a0.value)) { - return new OperatorNode('-', 'unaryMinus', [a1]) - } + } + if (node.op === '-') { + if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { + return simplifyCore( + new OperatorNode('+', 'add', [a0, a1.args[0]]), options) } - // if (node.fn === "subtract" && node.args.length === 2) { - if (node.fn === 'subtract') { - if (isConstantNode(a1) && isZero(a1.value)) { - return a0 - } - if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { - return simplifyCore( - new OperatorNode('+', 'add', [a0, a1.args[0]]), options) - } - return new OperatorNode(node.op, node.fn, [a0, a1]) + if (isConstantNode(a0) && isZero(a0.value)) { + return simplifyCore(new OperatorNode('-', 'unaryMinus', [a1])) + } + if (isConstantNode(a1) && isZero(a1.value)) { + return a0 } - } else if (node.op === '*') { + return new OperatorNode(node.op, node.fn, [a0, a1]) + } + if (node.op === '*') { if (isConstantNode(a0)) { if (isZero(a0.value)) { return node0 } else if (equal(a0.value, 1)) { return a1 - } else if (isConstantNode(a1)) { - return new ConstantNode(multiply(a0.value, a1.value)) } } if (isConstantNode(a1)) { @@ -211,55 +228,66 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ return node0 } else if (equal(a1.value, 1)) { return a0 - } else if (isOperatorNode(a0) && a0.isBinary() && - a0.op === node.op && isCommutative(node, context)) { - const a00 = a0.args[0] - if (isConstantNode(a00)) { - const a00a1 = new ConstantNode(multiply(a00.value, a1.value)) - return new OperatorNode(node.op, node.fn, [a00a1, a0.args[1]], node.implicit) // constants on left - } } if (isCommutative(node, context)) { return new OperatorNode(node.op, node.fn, [a1, a0], node.implicit) // constants on left - } else { - return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) } } return new OperatorNode(node.op, node.fn, [a0, a1], node.implicit) - } else if (node.op === '/') { - if (isConstantNode(a0)) { - if (isZero(a0.value)) { - return node0 - } else if (isConstantNode(a1) && - (equal(a1.value, 1) || equal(a1.value, 2) || equal(a1.value, 4))) { - return new ConstantNode(divide(a0.value, a1.value)) - } + } + if (node.op === '/') { + if (isConstantNode(a0) && isZero(a0.value)) { + return node0 + } + if (isConstantNode(a1) && equal(a1.value, 1)) { + return a0 } return new OperatorNode(node.op, node.fn, [a0, a1]) - } else if (node.op === '^') { + } + if (node.op === '^') { if (isConstantNode(a1)) { if (isZero(a1.value)) { return node1 } else if (equal(a1.value, 1)) { return a0 + } + } + } + if (node.op === 'and') { + if (isConstantNode(a0)) { + if (a0.value) { + if (isAlwaysBoolean(a1)) return a1 } else { - if (isConstantNode(a0)) { - // fold constant - return new ConstantNode(pow(a0.value, a1.value)) - } else if (isOperatorNode(a0) && a0.isBinary() && a0.op === '^') { - const a01 = a0.args[1] - if (isConstantNode(a01)) { - return new OperatorNode(node.op, node.fn, [ - a0.args[0], - new ConstantNode(multiply(a01.value, a1.value)) - ]) - } - } + return nodeF + } + } + if (isConstantNode(a1)) { + if (a1.value) { + if (isAlwaysBoolean(a0)) return a0 + } else { + return nodeF + } + } + } + if (node.op === 'or') { + if (isConstantNode(a0)) { + if (a0.value) { + return nodeT + } else { + if (isAlwaysBoolean(a1)) return a1 + } + } + if (isConstantNode(a1)) { + if (a1.value) { + return nodeT + } else { + if (isAlwaysBoolean(a0)) return a0 } } } return new OperatorNode(node.op, node.fn, [a0, a1]) - } else if (isOperatorNode(node)) { + } + if (isOperatorNode(node)) { return new OperatorNode(node.op, node.fn, node.args.map(a => simplifyCore(a, options))) } diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index cb04a413ae..101dec44a3 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -181,7 +181,7 @@ describe('simplify', function () { simplifyAndCompare('2 - -3', '5') let e = math.parse('2 - -3') e = math.simplifyCore(e) - assert.strictEqual(e.toString(), '5') // simplifyCore + assert.strictEqual(e.toString(), '2 + 3') // simplifyCore simplifyAndCompare('x - -x', '2*x') e = math.parse('x - -x') e = math.simplifyCore(e) @@ -212,7 +212,7 @@ describe('simplify', function () { simplifyAndCompareEval('1 - 1e-10', '1 - 1e-10') simplifyAndCompareEval('1 + 1e-10', '1 + 1e-10') simplifyAndCompareEval('1e-10 / 2', '1e-10 / 2') - simplifyAndCompareEval('(1e-5)^2', '(1e-5)^2') + simplifyAndCompareEval('(1e-5)^2', '1e-10') simplifyAndCompareEval('min(1, -1e-10)', '-1e-10') simplifyAndCompareEval('max(1e-10, -1)', '1e-10') }) @@ -286,7 +286,7 @@ describe('simplify', function () { it('should not run into an infinite recursive loop', function () { simplifyAndCompare('2n - 1', '2 n - 1') simplifyAndCompare('16n - 1', '16 n - 1') - simplifyAndCompare('16n / 1', '16 * n') + simplifyAndCompare('16n / 1', '16 n') simplifyAndCompare('8 / 5n', 'n * 8 / 5') simplifyAndCompare('8n - 4n', '4 * n') simplifyAndCompare('8 - 4n', '8 - 4 * n') @@ -508,16 +508,20 @@ describe('simplify', function () { } } + // Simplify actually increases accuracy when it uses fractions, so we + // disable that to get equality in these tests: + realContext.exactFractions = false + positiveContext.exactFractions = false for (const textExpr of expLibrary) { const expr = math.parse(textExpr) const realex = math.simplify(expr, {}, realContext) const posex = math.simplify(expr, {}, positiveContext) - assertAlike(expr.evaluate(zeroes), realex.evaluate(zeroes)) - assertAlike(expr.evaluate(negones), realex.evaluate(negones)) - assertAlike(expr.evaluate(ones), realex.evaluate(ones)) - assertAlike(expr.evaluate(twos), realex.evaluate(twos)) - assertAlike(expr.evaluate(ones), posex.evaluate(ones)) - assertAlike(expr.evaluate(twos), posex.evaluate(twos)) + assertAlike(realex.evaluate(zeroes), expr.evaluate(zeroes)) + assertAlike(realex.evaluate(negones), expr.evaluate(negones)) + assertAlike(realex.evaluate(ones), expr.evaluate(ones)) + assertAlike(realex.evaluate(twos), expr.evaluate(twos)) + assertAlike(posex.evaluate(ones), expr.evaluate(ones)) + assertAlike(posex.evaluate(twos), expr.evaluate(twos)) } // Make sure at least something is not equal const expr = math.parse('x/x') diff --git a/test/unit-tests/function/algebra/simplifyConstant.test.js b/test/unit-tests/function/algebra/simplifyConstant.test.js new file mode 100644 index 0000000000..5d643f4365 --- /dev/null +++ b/test/unit-tests/function/algebra/simplifyConstant.test.js @@ -0,0 +1,38 @@ +// test simplifyConstant +import assert from 'assert' + +import math from '../../../../src/defaultInstance.js' + +describe('simplifyConstant', function () { + const testSimplifyConstant = function (expr, expected, opts = {}, simpOpts = {}) { + let actual = math.simplifyConstant(math.parse(expr), simpOpts).toString(opts) + assert.strictEqual(actual, expected) + actual = math.simplifyConstant(expr, simpOpts).toString(opts) + assert.strictEqual(actual, expected) + } + + it('should evaluate constant subexpressions', () => { + testSimplifyConstant('2+2', '4') + testSimplifyConstant('x+3*5', 'x + 15') + testSimplifyConstant('f(sin(0))', 'f(0)') + testSimplifyConstant('[10/2, y, 8-4]', '[5, y, 4]') + }) + + it('should by default convert decimals into fractions', () => { + testSimplifyConstant('0.5 x', '1 / 2 x') + }) + + it('should coalesce constants in a multi-argument expression', () => { + testSimplifyConstant('3 + x + 7 + y', '10 + x + y') + testSimplifyConstant('3 * x * 7 * y', '21 * x * y') + }) + + it('should respect simplify options', () => { + testSimplifyConstant('0.5 x', '0.5 * x', { implicit: 'show' }, + { exactFractions: false }) + testSimplifyConstant('0.001 x', '0.001 * x', { implicit: 'show' }, + { fractionsLimit: 999 }) + testSimplifyConstant('3 * x * 7 * y', '3 * x * 7 * y', {}, + { context: { multiply: { commutative: false } } }) + }) +}) diff --git a/test/unit-tests/function/algebra/simplifyCore.test.js b/test/unit-tests/function/algebra/simplifyCore.test.js index 2bd0666050..06086c4643 100644 --- a/test/unit-tests/function/algebra/simplifyCore.test.js +++ b/test/unit-tests/function/algebra/simplifyCore.test.js @@ -12,8 +12,8 @@ describe('simplifyCore', function () { } it('should handle different node types', function () { - testSimplifyCore('5*x*3', '15 * x') - testSimplifyCore('5*x*3*x', '15 * x * x') + testSimplifyCore('5*x*3', '3 * 5 * x') + testSimplifyCore('5*x*3*x', '3 * 5 * x * x') testSimplifyCore('x-0', 'x') testSimplifyCore('0-x', '-x') @@ -26,6 +26,16 @@ describe('simplifyCore', function () { testSimplifyCore('1*x', 'x') testSimplifyCore('-(x)', '-x') testSimplifyCore('0/x', '0') + testSimplifyCore('~~(a | b)', 'a | b') + testSimplifyCore('not (not (p and q))', 'p and q') + testSimplifyCore('1 and not done', 'not done') + testSimplifyCore('false and you(know, it)', 'false') + testSimplifyCore('(p or q) and "you"', 'p or q') + testSimplifyCore('something and ""', 'false') + testSimplifyCore('false or not(way)', 'not way') + testSimplifyCore('6 or dozen/2', 'true') + testSimplifyCore('(a and b) or 0', 'a and b') + testSimplifyCore('consequences or true', 'true') testSimplifyCore('(1*x + y*0)*1+0', 'x') testSimplifyCore('sin(x+0)*1', 'sin(x)') testSimplifyCore('((x+0)*1)', 'x') @@ -53,15 +63,8 @@ describe('simplifyCore', function () { testSimplifyCore('x+(y+z)+w', '(x + (y + z)) + w', { parenthesis: 'all' }) }) - it('folds constants', function () { - testSimplifyCore('1+2', '3') - testSimplifyCore('2*3', '6') - testSimplifyCore('2-3', '-1') - testSimplifyCore('3/2', '1.5') - testSimplifyCore('3^2', '9') - }) - it('should convert +unaryMinus to subtract', function () { + testSimplifyCore('x + -1', 'x - 1') const result = math.simplify( 'x + y + a', [math.simplifyCore], { a: -1 } ).toString() @@ -85,4 +88,11 @@ describe('simplifyCore', function () { testSimplifyCore('and(multiply(1, x), true)', 'x and true') testSimplifyCore('add(x, 0 ,y)', 'x + y') }) + + it('can perform sequential distinct core simplifications', () => { + testSimplifyCore('0 - -x', 'x') + testSimplifyCore('0 - (x - y)', 'y - x') + testSimplifyCore('a + -0', 'a') + testSimplifyCore('-(-x - y)', 'y + x') + }) }) diff --git a/types/index.d.ts b/types/index.d.ts index 8d1e6fc31f..5f9bc64267 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -723,6 +723,9 @@ declare namespace math { */ simplify: Simplify; + simplifyConstant(expr: MathNode | string, options?: SimplifyOptions); + simplifyCore(expr: MathNode | string, options?: SimplifyOptions); + /** * Calculate the Sparse Matrix LU decomposition with full pivoting. * Sparse Matrix A is decomposed in two matrices (L, U) and two @@ -3288,6 +3291,8 @@ declare namespace math { * Default value is 10000. */ fractionsLimit?: number; + consoleDebug?: boolean; + context?: object; } type SimplifyRule = { l: string; r: string } | string | ((node: MathNode) => MathNode); @@ -3761,7 +3766,8 @@ declare namespace math { */ simplify(rules?: SimplifyRule[], scope?: object): MathJsChain; - simplifyCore(expr: MathNode): MathNode; + simplifyConstant(options?: SimplifyOptions): MathJsChain; + simplifyCore(options?: SimplifyOptions): MathJsChain; /** * Calculate the Sparse Matrix LU decomposition with full pivoting. diff --git a/types/index.ts b/types/index.ts index 02070d38d3..e3b686610a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -135,13 +135,19 @@ Simplify examples const math = create(all); math.simplify("2 * 1 * x ^ (2 - 1)"); + math.simplifyConstant("2 * 1 * x ^ (2 - 1)"); + math.simplifyCore("2 * 1 * x ^ (2 - 1)"); math.simplify("2 * 3 * x", { x: 4 }); const f = math.parse("2 * 1 * x ^ (2 - 1)"); math.simplify(f); math.simplify("0.4 * x", {}, { exactFractions: true }); + math.simplifyConstant("0.4 * x", { exactFractions: true }); math.simplify("0.4 * x", {}, { exactFractions: false }); + math.simplifyCore("0.4 * x + 0", { exactFractions: false }); + + math.chain("0.4 * x + 0").parse().simplifyCore({exactFractions: false}).simplifyConstant(); } /*