From c5315340a20e0496fe54f6546f1186cf5ff604b8 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 ++++++++++-------- .../treeShaking/treeShaking.test.js | 1 - .../function/algebra/simplify.test.js | 22 ++- .../function/algebra/simplifyConstant.test.js | 38 ++++ .../function/algebra/simplifyCore.test.js | 30 ++- types/index.d.ts | 14 +- types/index.ts | 6 + 15 files changed, 265 insertions(+), 142 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 34ff8092d2..19779e38f5 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' @@ -338,6 +339,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 6ff5739af3..2ffc291c23 100644 --- a/src/factoriesAny.js +++ b/src/factoriesAny.js @@ -248,6 +248,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 6531170363..9ae85a9b40 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/node-tests/treeShaking/treeShaking.test.js b/test/node-tests/treeShaking/treeShaking.test.js index 92a947d6aa..874935cd0c 100644 --- a/test/node-tests/treeShaking/treeShaking.test.js +++ b/test/node-tests/treeShaking/treeShaking.test.js @@ -66,7 +66,6 @@ describe('tree shaking', function () { assert.strictEqual(info.assets[0].name, bundleName) const size = info.assets[0].size const maxSize = 120000 - const maxSize = 111000 assert(size < maxSize, 'bundled size must be small enough ' + '(actual size: ' + size + ' bytes, max size: ' + maxSize + ' bytes)') diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index 6250677465..0e82a1a9d2 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -182,7 +182,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) @@ -213,7 +213,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') }) @@ -287,7 +287,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') @@ -510,16 +510,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 e478cf607d..44cbc52c27 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 24a5472761..6090d30cf6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -890,6 +890,9 @@ declare namespace math { */ simplify: Simplify + simplifyConstant(expr: MathNode | string, options?: SimplifyOptions); + simplifyCore(expr: MathNode | string, options?: SimplifyOptions); + /** * Replaces variable nodes with their scoped values * @param node Tree to replace variable nodes in @@ -4352,11 +4355,16 @@ declare namespace math { simplify( this: MathJsChain, rules?: SimplifyRule[], - scope?: object + scope?: Map | object, + options?: SimplifyOptions ): MathJsChain - // TODO check that this should even be here... - simplifyCore(expr: MathNode): MathNode + simplifyConstant( + this: MathJsChain, + options?: SimplifyOptions): MathJsChain + simplifyCore( + this: MathJsChain, + options?: SimplifyOptions): MathJsChain /** * Calculate the Sparse Matrix LU decomposition with full pivoting. diff --git a/types/index.ts b/types/index.ts index ded86474fd..7e29a5d834 100644 --- a/types/index.ts +++ b/types/index.ts @@ -776,6 +776,8 @@ 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 }) expectTypeOf(math.simplify.rules).toMatchTypeOf> @@ -784,6 +786,7 @@ Simplify examples 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.simplify('0.4 * x', {}, { fractionsLimit: 2 }) math.simplify('0.4 * x', {}, { consoleDebug: false }) @@ -849,6 +852,9 @@ Simplify examples }, (node: MathNode) => node, ]) + math.simplifyCore('0.4 * x + 0', { exactFractions: false }) + + math.chain('0.4 * x + 0').parse().simplifyCore({exactFractions: false}).simplifyConstant() } /*