diff --git a/HISTORY.md b/HISTORY.md index 1941ef91a..8c05e6898 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,8 @@ errors for larger values, see #2414. Thanks @gwhitney. - Fix #2385: function `rotate` missing in TypeScript definitions. Thanks @DIVYA-19. +- Fix #2450: Add BigNumber to parameter type in `math.unit` and add TypeScript + types for `Unit.simplify` and `Unit.units` (#2353). Thanks @joshhansen. # 2022-02-02, version 10.1.1 diff --git a/src/expression/Help.js b/src/expression/Help.js index 5b7e7e2b6..b466bc99d 100644 --- a/src/expression/Help.js +++ b/src/expression/Help.js @@ -76,6 +76,9 @@ export const createHelpClass = /* #__PURE__ */ factory(name, dependencies, ({ pa } desc += '\n' } + if (doc.mayThrow && doc.mayThrow.length) { + desc += 'Throws: ' + doc.mayThrow.join(', ') + '\n\n' + } if (doc.seealso && doc.seealso.length) { desc += 'See also: ' + doc.seealso.join(', ') + '\n' } diff --git a/src/expression/embeddedDocs/embeddedDocs.js b/src/expression/embeddedDocs/embeddedDocs.js index 8020b5935..590aeddc5 100644 --- a/src/expression/embeddedDocs/embeddedDocs.js +++ b/src/expression/embeddedDocs/embeddedDocs.js @@ -178,6 +178,8 @@ import { sluDocs } from './function/algebra/slu.js' import { leafCountDocs } from './function/algebra/leafCount.js' import { rationalizeDocs } from './function/algebra/rationalize.js' import { simplifyDocs } from './function/algebra/simplify.js' +import { simplifyCoreDocs } from './function/algebra/simplifyCore.js' +import { resolveDocs } from './function/algebra/resolve.js' import { lupDocs } from './function/algebra/lup.js' import { lsolveDocs } from './function/algebra/lsolve.js' import { lsolveAllDocs } from './function/algebra/lsolveAll.js' @@ -329,6 +331,8 @@ export const embeddedDocs = { lusolve: lusolveDocs, leafCount: leafCountDocs, simplify: simplifyDocs, + resolve: resolveDocs, + simplifyCore: simplifyCoreDocs, rationalize: rationalizeDocs, slu: sluDocs, usolve: usolveDocs, diff --git a/src/expression/embeddedDocs/function/algebra/resolve.js b/src/expression/embeddedDocs/function/algebra/resolve.js new file mode 100644 index 000000000..abd73dfc0 --- /dev/null +++ b/src/expression/embeddedDocs/function/algebra/resolve.js @@ -0,0 +1,20 @@ +export const resolveDocs = { + name: 'resolve', + category: 'Algebra', + syntax: [ + 'resolve(node, scope)' + ], + description: 'Recursively substitute variables in an expression tree.', + examples: [ + 'resolve(parse("1 + x"), { x: 7 })', + 'resolve(parse("size(text)"), { text: "Hello World" })', + 'resolve(parse("x + y"), { x: parse("3z") })', + 'resolve(parse("3x"), { x: parse("y+z"), z: parse("w^y") })' + ], + seealso: [ + 'simplify', 'evaluate' + ], + mayThrow: [ + 'ReferenceError' + ] +} diff --git a/src/expression/embeddedDocs/function/algebra/simplify.js b/src/expression/embeddedDocs/function/algebra/simplify.js index 2606b45e4..36a38364b 100644 --- a/src/expression/embeddedDocs/function/algebra/simplify.js +++ b/src/expression/embeddedDocs/function/algebra/simplify.js @@ -14,6 +14,6 @@ export const simplifyDocs = { 'simplified.evaluate({x: 2})' ], seealso: [ - 'derivative', 'parse', 'evaluate' + 'simplifyCore', 'derivative', 'evaluate', 'parse', 'rationalize', 'resolve' ] } diff --git a/src/expression/embeddedDocs/function/algebra/simplifyCore.js b/src/expression/embeddedDocs/function/algebra/simplifyCore.js new file mode 100644 index 000000000..6d967847f --- /dev/null +++ b/src/expression/embeddedDocs/function/algebra/simplifyCore.js @@ -0,0 +1,15 @@ +export const simplifyCoreDocs = { + name: 'simplifyCore', + category: 'Algebra', + syntax: [ + 'simplifyCore(node)' + ], + description: 'Perform simple one-pass simplifications on an expression tree.', + examples: [ + 'simplifyCore(parse("0*x"))', + 'simplifyCore(parse("(x+0)*2"))' + ], + seealso: [ + 'simplify', 'evaluate' + ] +} diff --git a/src/factoriesAny.js b/src/factoriesAny.js index 7b81b6ead..473ec8f1c 100644 --- a/src/factoriesAny.js +++ b/src/factoriesAny.js @@ -243,6 +243,8 @@ 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 { createSimplifyCore } from './function/algebra/simplifyCore.js' +export { createResolve } from './function/algebra/resolve.js' export { createDerivative } from './function/algebra/derivative.js' export { createRationalize } from './function/algebra/rationalize.js' export { createReviver } from './json/reviver.js' diff --git a/src/factoriesNumber.js b/src/factoriesNumber.js index 241181a30..3143f6243 100644 --- a/src/factoriesNumber.js +++ b/src/factoriesNumber.js @@ -89,7 +89,9 @@ export { createHelp } from './expression/function/help.js' export { createChain } from './type/chain/function/chain.js' // algebra +export { createResolve } from './function/algebra/resolve.js' export { createSimplify } from './function/algebra/simplify.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 a55f38284..3a0202488 100644 --- a/src/function/algebra/rationalize.js +++ b/src/function/algebra/rationalize.js @@ -1,7 +1,6 @@ import { isInteger } from '../../utils/number.js' import { factory } from '../../utils/factory.js' import { createSimplifyConstant } from './simplify/simplifyConstant.js' -import { createSimplifyCore } from './simplify/simplifyCore.js' const name = 'rationalize' const dependencies = [ @@ -15,6 +14,7 @@ const dependencies = [ 'divide', 'pow', 'parse', + 'simplifyCore', 'simplify', '?bignumber', '?fraction', @@ -42,6 +42,7 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({ divide, pow, parse, + simplifyCore, simplify, fraction, bignumber, @@ -73,24 +74,6 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({ OperatorNode, SymbolNode }) - const simplifyCore = createSimplifyCore({ - equal, - isZero, - add, - subtract, - multiply, - divide, - pow, - AccessorNode, - ArrayNode, - ConstantNode, - FunctionNode, - IndexNode, - ObjectNode, - OperatorNode, - ParenthesisNode, - SymbolNode - }) /** * Transform a rationalizable expression in a rational fraction. diff --git a/src/function/algebra/resolve.js b/src/function/algebra/resolve.js new file mode 100644 index 000000000..2ee8e1988 --- /dev/null +++ b/src/function/algebra/resolve.js @@ -0,0 +1,94 @@ +import { createMap, isMap } from '../../utils/map.js' +import { isFunctionNode, isNode, isOperatorNode, isParenthesisNode, isSymbolNode } from '../../utils/is.js' +import { factory } from '../../utils/factory.js' + +const name = 'resolve' +const dependencies = [ + 'parse', + 'ConstantNode', + 'FunctionNode', + 'OperatorNode', + 'ParenthesisNode' +] + +export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ + parse, + ConstantNode, + FunctionNode, + OperatorNode, + ParenthesisNode +}) => { + /** + * resolve(expr, scope) replaces variable nodes with their scoped values + * + * Syntax: + * + * resolve(expr, scope) + * + * Examples: + * + * math.resolve('x + y', {x:1, y:2}) // Node {1 + 2} + * math.resolve(math.parse('x+y'), {x:1, y:2}) // Node {1 + 2} + * math.simplify('x+y', {x:2, y:'x+x'}).toString() // "6" + * + * See also: + * + * simplify, evaluate + * + * @param {Node} node + * The expression tree to be simplified + * @param {Object} scope + * Scope specifying variables to be resolved + * @return {Node} Returns `node` with variables recursively substituted. + * @throws {ReferenceError} + * If there is a cyclic dependency among the variables in `scope`, + * resolution is impossible and a ReferenceError is thrown. + */ + function resolve (node, scope, within = new Set()) { // note `within`: + // `within` is not documented, since it is for internal cycle + // detection only + if (!scope) { + return node + } + if (!isMap(scope)) { + scope = createMap(scope) + } + if (isSymbolNode(node)) { + if (within.has(node.name)) { + const variables = Array.from(within).join(', ') + throw new ReferenceError( + `recursive loop of variable definitions among {${variables}}` + ) + } + const value = scope.get(node.name) + if (isNode(value)) { + const nextWithin = new Set(within) + nextWithin.add(node.name) + return resolve(value, scope, nextWithin) + } else if (typeof value === 'number') { + return parse(String(value)) + } else if (value !== undefined) { + return new ConstantNode(value) + } else { + return node + } + } else if (isOperatorNode(node)) { + const args = node.args.map(function (arg) { + return resolve(arg, scope, within) + }) + return new OperatorNode(node.op, node.fn, args, node.implicit) + } else if (isParenthesisNode(node)) { + return new ParenthesisNode(resolve(node.content, scope, within)) + } else if (isFunctionNode(node)) { + const args = node.args.map(function (arg) { + return resolve(arg, scope, within) + }) + return new FunctionNode(node.name, args) + } + // Otherwise just recursively resolve any children (might also work + // for some of the above special cases) + return node.map(child => resolve(child, scope, within)) + } + + return resolve +}) diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index 9a45ec630..4c9ac5c0c 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -1,9 +1,7 @@ import { isConstantNode, isParenthesisNode } from '../../utils/is.js' import { factory } from '../../utils/factory.js' import { createUtil } from './simplify/util.js' -import { createSimplifyCore } from './simplify/simplifyCore.js' import { createSimplifyConstant } from './simplify/simplifyConstant.js' -import { createResolve } from './simplify/resolve.js' import { hasOwnProperty } from '../../utils/object.js' import { createEmptyMap, createMap } from '../../utils/map.js' @@ -19,6 +17,8 @@ const dependencies = [ 'pow', 'isZero', 'equal', + 'resolve', + 'simplifyCore', '?fraction', '?bignumber', 'mathWithTransform', @@ -46,6 +46,8 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( pow, isZero, equal, + resolve, + simplifyCore, fraction, bignumber, mathWithTransform, @@ -77,30 +79,6 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( OperatorNode, SymbolNode }) - const simplifyCore = createSimplifyCore({ - equal, - isZero, - add, - subtract, - multiply, - divide, - pow, - AccessorNode, - ArrayNode, - ConstantNode, - FunctionNode, - IndexNode, - ObjectNode, - OperatorNode, - ParenthesisNode, - SymbolNode - }) - const resolve = createResolve({ - parse, - FunctionNode, - OperatorNode, - ParenthesisNode - }) const { hasProperty, isCommutative, isAssociative, mergeContext, flatten, unflattenr, unflattenl, createMakeNodeFunction, defaultContext, realContext, positiveContext } = createUtil({ FunctionNode, OperatorNode, SymbolNode }) @@ -200,7 +178,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( * * See also: * - * derivative, parse, evaluate, rationalize + * simplifyCore, derivative, evaluate, parse, rationalize, resolve * * @param {Node | string} expr * The expression to be simplified @@ -298,8 +276,6 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( return res } }) - simplify.simplifyCore = simplifyCore - simplify.resolve = resolve simplify.defaultContext = defaultContext simplify.realContext = realContext simplify.positiveContext = positiveContext @@ -374,7 +350,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { multiply: { commutative: false }, subtract: { total: true } } }, { l: '-(n1/n2)', r: '-n1/n2' }, - { l: '-v', r: 'v * (-1)' }, + { l: '-v', r: 'v * (-1)' }, // finish making non-constant terms positive { l: '(n1 + n2)*(-1)', r: 'n1*(-1) + n2*(-1)', repeat: true }, // expand negations to achieve as much sign cancellation as possible { l: 'n/n1^n2', r: 'n*n1^-n2' }, // temporarily replace 'divide' so we can further flatten the 'multiply' operator { l: 'n/n1', r: 'n*n1^-1' }, @@ -387,15 +363,26 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { multiply: { commutative: false } } }, - simplifyConstant, - // expand nested exponentiation { s: '(n ^ n1) ^ n2 -> n ^ (n1 * n2)', assuming: { divide: { total: true } } // 1/(1/n) = n needs 1/n to exist }, - // collect like factors + // collect like factors; into a sum, only do this for nonconstants + { l: ' v * ( v * n1 + n2)', r: 'v^2 * n1 + v * n2' }, + { + s: ' v * (v^n4 * n1 + n2) -> v^(1+n4) * n1 + v * n2', + assuming: { divide: { total: true } } // v*1/v = v^(1+-1) needs 1/v + }, + { + s: 'v^n3 * ( v * n1 + n2) -> v^(n3+1) * n1 + v^n3 * n2', + assuming: { divide: { total: true } } + }, + { + s: 'v^n3 * (v^n4 * n1 + n2) -> v^(n3+n4) * n1 + v^n3 * n2', + assuming: { divide: { total: true } } + }, { l: 'n*n', r: 'n^2' }, { s: 'n * n^n1 -> n^(n1+1)', @@ -406,6 +393,12 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { divide: { total: true } } // ditto for n^2*1/n^2 }, + // Unfortunately, to deal with more complicated cancellations, it + // becomes necessary to simplify constants twice per pass. It's not + // terribly expensive compared to matching rules, so this should not + // pose a performance problem. + simplifyConstant, // First: before collecting like terms + // collect like terms { s: 'n+n -> 2*n', @@ -414,6 +407,8 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( { l: 'n+-n', r: '0' }, { l: 'v*n + v', r: 'v*(n+1)' }, // NOTE: leftmost position is special: { l: 'n3*n1 + n3*n2', r: 'n3*(n1+n2)' }, // All sub-monomials tried there. + { l: 'n3^(-n4)*n1 + n3 * n2', r: 'n3^(-n4)*(n1 + n3^(n4+1) *n2)' }, + { l: 'n3^(-n4)*n1 + n3^n5 * n2', r: 'n3^(-n4)*(n1 + n3^(n4+n5)*n2)' }, { s: 'n*v + v -> (n+1)*v', // noncommutative additional cases assuming: { multiply: { commutative: false } } @@ -422,12 +417,22 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( s: 'n1*n3 + n2*n3 -> (n1+n2)*n3', assuming: { multiply: { commutative: false } } }, + { + s: 'n1*n3^(-n4) + n2 * n3 -> (n1 + n2*n3^(n4 + 1))*n3^(-n4)', + assuming: { multiply: { commutative: false } } + }, + { + s: 'n1*n3^(-n4) + n2 * n3^n5 -> (n1 + n2*n3^(n4 + n5))*n3^(-n4)', + assuming: { multiply: { commutative: false } } + }, { l: 'n*c + c', r: '(n+1)*c' }, { s: 'c*n + c -> c*(n+1)', assuming: { multiply: { commutative: false } } }, + simplifyConstant, // Second: before returning expressions to "standard form" + // make factors positive (and undo 'make non-constant terms positive') { s: '(-n)*n1 -> -(n*n1)', @@ -462,10 +467,10 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( assuming: { multiply: { commutative: true } } // o.w. / not conventional }, { - s: 'n1^-1 -> 1/n1', + s: 'n^-1 -> 1/n', assuming: { multiply: { commutative: true } } // o.w. / not conventional }, - + { l: 'n^1', r: 'n' }, // can be produced by power cancellation { s: 'n*(n1/n2) -> (n*n1)/n2', // '*' before '/' assuming: { multiply: { associative: true } } diff --git a/src/function/algebra/simplify/resolve.js b/src/function/algebra/simplify/resolve.js deleted file mode 100644 index 879d8ddb7..000000000 --- a/src/function/algebra/simplify/resolve.js +++ /dev/null @@ -1,67 +0,0 @@ -import { createMap, isMap } from '../../../utils/map.js' -import { isFunctionNode, isNode, isOperatorNode, isParenthesisNode, isSymbolNode } from '../../../utils/is.js' -import { factory } from '../../../utils/factory.js' - -const name = 'resolve' -const dependencies = [ - 'parse', - 'FunctionNode', - 'OperatorNode', - 'ParenthesisNode' -] - -export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ - parse, - FunctionNode, - OperatorNode, - ParenthesisNode -}) => { - /** - * resolve(expr, scope) replaces variable nodes with their scoped values - * - * Syntax: - * - * simplify.resolve(expr, scope) - * - * Examples: - * - * math.simplify.resolve('x + y', {x:1, y:2}) // Node {1 + 2} - * math.simplify.resolve(math.parse('x+y'), {x:1, y:2}) // Node {1 + 2} - * math.simplify('x+y', {x:2, y:'x+x'}).toString() // "6" - * - * @param {Node} node - * The expression tree to be simplified - * @param {Object} scope with variables to be resolved - */ - function resolve (node, scope) { - if (!scope) { - return node - } - if (!isMap(scope)) { - scope = createMap(scope) - } - if (isSymbolNode(node)) { - const value = scope.get(node.name) - if (isNode(value)) { - return resolve(value, scope) - } else if (typeof value === 'number') { - return parse(String(value)) - } - } else if (isOperatorNode(node)) { - const args = node.args.map(function (arg) { - return resolve(arg, scope) - }) - return new OperatorNode(node.op, node.fn, args, node.implicit) - } else if (isParenthesisNode(node)) { - return new ParenthesisNode(resolve(node.content, scope)) - } else if (isFunctionNode(node)) { - const args = node.args.map(function (arg) { - return resolve(arg, scope) - }) - return new FunctionNode(node.name, args) - } - return node - } - - return resolve -}) diff --git a/src/function/algebra/simplify/simplifyCore.js b/src/function/algebra/simplifyCore.js similarity index 94% rename from src/function/algebra/simplify/simplifyCore.js rename to src/function/algebra/simplifyCore.js index bb70d9885..71cda16b4 100644 --- a/src/function/algebra/simplify/simplifyCore.js +++ b/src/function/algebra/simplifyCore.js @@ -1,6 +1,6 @@ -import { isAccessorNode, isArrayNode, isConstantNode, isFunctionNode, isIndexNode, isObjectNode, isOperatorNode } from '../../../utils/is.js' -import { createUtil } from './util.js' -import { factory } from '../../../utils/factory.js' +import { isAccessorNode, isArrayNode, isConstantNode, isFunctionNode, isIndexNode, isObjectNode, isOperatorNode } from '../../utils/is.js' +import { createUtil } from './simplify/util.js' +import { factory } from '../../utils/factory.js' const name = 'simplifyCore' const dependencies = [ @@ -53,22 +53,23 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ * * Syntax: * - * simplify.simplifyCore(expr) + * simplifyCore(expr) * * Examples: * * const f = math.parse('2 * 1 * x ^ (2 - 1)') - * math.simplify.simpifyCore(f) // Node {2 * x} - * math.simplify('2 * 1 * x ^ (2 - 1)', [math.simplify.simpifyCore]) // Node {2 * x} + * math.simpifyCore(f) // Node {2 * x} + * math.simplify('2 * 1 * x ^ (2 - 1)', [math.simplifyCore]) // Node {2 * x} * * See also: * - * derivative + * simplify, resolve, derivative * * @param {Node} node * The expression to be simplified * @param {Object} options * Simplification options, as per simplify() + * @return {Node} Returns expression with basic simplifications applied */ function simplifyCore (node, options) { const context = options ? options.context : undefined diff --git a/test/unit-tests/function/algebra/resolve.test.js b/test/unit-tests/function/algebra/resolve.test.js new file mode 100644 index 000000000..4e0748bf3 --- /dev/null +++ b/test/unit-tests/function/algebra/resolve.test.js @@ -0,0 +1,64 @@ +// test resolve +import assert from 'assert' + +import math from '../../../../src/defaultInstance.js' + +import { simplifyAndCompare } from './simplify.test.js' + +describe('resolve', function () { + it('should substitute scoped constants', function () { + const sumxy = math.parse('x+y') + const collapsingScope = { x: math.parse('y'), y: math.parse('z') } + assert.strictEqual( + math.resolve(sumxy, { x: 1 }).toString(), + '1 + y' + ) // direct + assert.strictEqual( + math.resolve(sumxy, collapsingScope).toString(), + 'z + z' + ) + assert.strictEqual( + math.resolve( + math.parse('[x,y,1,w]'), collapsingScope).toString(), + '[z, z, 1, w]' + ) + simplifyAndCompare('x+y', 'x+y', {}) // operator + simplifyAndCompare('x+y', 'y+1', { x: 1 }) + simplifyAndCompare('x+y', 'y+1', { x: math.parse('1') }) + simplifyAndCompare('x+y', '3', { x: 1, y: 2 }) + simplifyAndCompare('x+x+x', '3*x') + simplifyAndCompare('y', 'x+1', { y: math.parse('1+x') }) + simplifyAndCompare('y', '3', { x: 2, y: math.parse('1+x') }) + simplifyAndCompare('x+y', '3*x', { y: math.parse('x+x') }) + simplifyAndCompare('x+y', '6', { x: 2, y: math.parse('x+x') }) + simplifyAndCompare('x+(y+2-1-1)', '6', { x: 2, y: math.parse('x+x') }) // parentheses + simplifyAndCompare('log(x+y)', String(Math.log(6)), { x: 2, y: math.parse('x+x') }) // function + simplifyAndCompare('combinations( ceil(abs(sin(x)) * (y+3)), abs(x) )', + 'combinations(ceil(0.9092974268256817 * (y + 3) ), 2)', { x: -2 }) + + simplifyAndCompare('size(text)[1]', '11', { text: 'hello world' }) + }) + + it('should substitute scoped constants from Map like scopes', function () { + assert.strictEqual( + math.resolve(math.parse('x+y'), new Map([['x', 1]])).toString(), '1 + y' + ) // direct + simplifyAndCompare('x+y', 'x+y', new Map()) // operator + simplifyAndCompare('x+y', 'y+1', new Map([['x', 1]])) + simplifyAndCompare('x+y', 'y+1', new Map([['x', math.parse('1')]])) + }) + + it('should throw an error in case of reference loop', function () { + const sumxy = math.parse('x+y') + assert.throws( + () => math.resolve(sumxy, { x: math.parse('x') }), + /ReferenceError.*\{x\}/) + assert.throws( + () => math.resolve(sumxy, { + y: math.parse('3z'), + z: math.parse('1-x'), + x: math.parse('cos(y)') + }), + /ReferenceError.*\{x, y, z\}/) + }) +}) diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index 0478f831b..4bbf55e5d 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -3,44 +3,44 @@ import assert from 'assert' import math from '../../../../src/defaultInstance.js' -describe('simplify', function () { - const expLibrary = [] - function simplifyAndCompare (left, right, rules, scope, opt, stringOpt) { - expLibrary.push(left) - let simpLeft - try { - if (Array.isArray(rules)) { - if (opt) { - simpLeft = math.simplify(left, rules, scope, opt) - } else if (scope) { - simpLeft = math.simplify(left, rules, scope) - } else { - simpLeft = math.simplify(left, rules) - } +const expLibrary = [] +export function simplifyAndCompare (left, right, rules, scope, opt, stringOpt) { + expLibrary.push(left) + let simpLeft + try { + if (Array.isArray(rules)) { + if (opt) { + simpLeft = math.simplify(left, rules, scope, opt) + } else if (scope) { + simpLeft = math.simplify(left, rules, scope) } else { - if (opt) stringOpt = opt - if (scope) opt = scope - if (rules) scope = rules - if (opt) { - simpLeft = math.simplify(left, scope, opt) - } else if (scope) { - simpLeft = math.simplify(left, scope) - } else { - simpLeft = math.simplify(left) - } + simpLeft = math.simplify(left, rules) } - } catch (err) { - if (err instanceof Error) { - console.log(err.stack) + } else { + if (opt) stringOpt = opt + if (scope) opt = scope + if (rules) scope = rules + if (opt) { + simpLeft = math.simplify(left, scope, opt) + } else if (scope) { + simpLeft = math.simplify(left, scope) } else { - console.log(new Error(err)) + simpLeft = math.simplify(left) } - throw err } - assert.strictEqual( - simpLeft.toString(stringOpt), math.parse(right).toString(stringOpt)) + } catch (err) { + if (err instanceof Error) { + console.log(err.stack) + } else { + console.log(new Error(err)) + } + throw err } + assert.strictEqual( + simpLeft.toString(stringOpt), math.parse(right).toString(stringOpt)) +} +describe('simplify', function () { function simplifyAndCompareEval (left, right, scope) { expLibrary.push(left) scope = scope || {} @@ -101,65 +101,13 @@ describe('simplify', function () { const f = new math.FunctionAssignmentNode('sigma', ['x'], math.parse('1 / (1 + exp(-x))')) assert.strictEqual(f.toString(), 'sigma(x) = 1 / (1 + exp(-x))') assert.strictEqual(f.evaluate()(5), 0.9933071490757153) - const fsimplified = math.simplify.simplifyCore(f) + const fsimplified = math.simplifyCore(f) assert.strictEqual(fsimplified.toString(), 'sigma(x) = 1 / (1 + exp(-x))') assert.strictEqual(fsimplified.evaluate()(5), 0.9933071490757153) }) - const testSimplifyCore = function (expr, expected, opts = {}) { - const actual = math.simplify.simplifyCore(math.parse(expr)).toString(opts) - assert.strictEqual(actual, expected) - } - - it('simplifyCore should handle different node types', function () { - testSimplifyCore('5*x*3', '15 * x') - testSimplifyCore('5*x*3*x', '15 * x * x') - - testSimplifyCore('x-0', 'x') - testSimplifyCore('0-x', '-x') - testSimplifyCore('0-3', '-3') - testSimplifyCore('x+0', 'x') - testSimplifyCore('0+x', 'x') - testSimplifyCore('0*x', '0') - testSimplifyCore('x*0', '0') - testSimplifyCore('x*1', 'x') - testSimplifyCore('1*x', 'x') - testSimplifyCore('-(x)', '-x') - testSimplifyCore('0/x', '0') - testSimplifyCore('(1*x + y*0)*1+0', 'x') - testSimplifyCore('sin(x+0)*1', 'sin(x)') - testSimplifyCore('((x+0)*1)', 'x') - testSimplifyCore('sin((x-0)*1+y*0)', 'sin(x)') - testSimplifyCore('[x+0,1*y,z*0]', '[x, y, 0]') - testSimplifyCore('(a+b+0)[n*0+1,-(n)]', '(a + b)[1, -n]') - testSimplifyCore('{a:x*1, b:y-0}', '{"a": x, "b": y}') - }) - - it('simplifyCore strips ParenthesisNodes (implicit in tree)', function () { - testSimplifyCore('((x)*(y))', 'x * y') - testSimplifyCore('((x)*(y))^1', 'x * y') - testSimplifyCore('x*(y+z)', 'x * (y + z)') - testSimplifyCore('x+(y+z)+w', 'x + y + z + w') - // But it doesn't actually change the association internally: - testSimplifyCore('x+ y+z +w', '((x + y) + z) + w', { parenthesis: 'all' }) - testSimplifyCore('x+(y+z)+w', '(x + (y + z)) + w', { parenthesis: 'all' }) - }) - - it('simplifyCore 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 simplifyCore convert +unaryMinus to subtract', function () { - simplifyAndCompareEval('--2', '2') - const result = math.simplify('x + y + a', [math.simplify.simplifyCore], { a: -1 }).toString() - assert.strictEqual(result, 'x + y - 1') - }) - it('should simplify convert minus and unary minus', function () { + simplifyAndCompareEval('--2', '2') // see https://github.com/josdejong/mathjs/issues/1013 assert.strictEqual(math.simplify('0 - -1', {}).toString(), '1') assert.strictEqual(math.simplify('0 - -x', {}).toString(), 'x') @@ -171,10 +119,10 @@ describe('simplify', function () { }) it('should simplify inside arrays and indexing', function () { - simplifyAndCompare('[3x+0]', '[3x]') // simplifyCore inside array + simplifyAndCompare('[3x+0]', '[3x]') simplifyAndCompare('[3x+5x]', '[8*x]') simplifyAndCompare('[2*3,6+2]', '[6,8]') - simplifyAndCompare('[x^0,y*0,z*1,w-0][2+n*1]', '[1,0,z,w][n+2]') // simplifyCore in index + simplifyAndCompare('[x^0,y*0,z*1,w-0][2+n*1]', '[1,0,z,w][n+2]') simplifyAndCompare('[x,y-2y,z,w+w][(3-2)*n+2]', '[x,-y,z,2*w][n+2]') }) @@ -212,7 +160,7 @@ describe('simplify', function () { const f = new math.FunctionNode(new math.SymbolNode('doubleIt'), [new math.SymbolNode('value')]) assert.strictEqual(f.toString(), 'doubleIt(value)') assert.strictEqual(f.evaluate({ doubleIt: doubleIt, value: 4 }), 8) - const fsimplified = math.simplify.simplifyCore(f) + const fsimplified = math.simplifyCore(f) assert.strictEqual(fsimplified.toString(), 'doubleIt(value)') assert.strictEqual(fsimplified.evaluate({ doubleIt: doubleIt, value: 4 }), 8) }) @@ -222,7 +170,7 @@ describe('simplify', function () { const f = new math.FunctionNode(s, [new math.SymbolNode('x')]) assert.strictEqual(f.toString(), '(sigma(x) = 1 / (1 + exp(-x)))(x)') assert.strictEqual(f.evaluate({ x: 5 }), 0.9933071490757153) - const fsimplified = math.simplify.simplifyCore(f) + const fsimplified = math.simplifyCore(f) assert.strictEqual(fsimplified.toString(), '(sigma(x) = 1 / (1 + exp(-x)))(x)') assert.strictEqual(fsimplified.evaluate({ x: 5 }), 0.9933071490757153) }) @@ -232,11 +180,11 @@ describe('simplify', function () { simplifyAndCompare('2 - 3', '-1') simplifyAndCompare('2 - -3', '5') let e = math.parse('2 - -3') - e = math.simplify.simplifyCore(e) + e = math.simplifyCore(e) assert.strictEqual(e.toString(), '5') // simplifyCore simplifyAndCompare('x - -x', '2*x') e = math.parse('x - -x') - e = math.simplify.simplifyCore(e) + e = math.simplifyCore(e) assert.strictEqual(e.toString(), 'x + x') // not a core simplification since + is cheaper than * }) @@ -301,6 +249,13 @@ describe('simplify', function () { simplifyAndCompare('x - (y - (y - x))', '0') simplifyAndCompare('5 + (5 * x) - (3 * x) + 2', '2*x+7') simplifyAndCompare('x^2*y^2 - (x*y)^2', '0') + simplifyAndCompare('(x*z^2 + y*z)/z^4', '(y + z*x)/z^3') // #1423 + simplifyAndCompare('(x^2*y + z*y)/y^4', '(x^2 + z)/y^3') + simplifyAndCompare('6x/3x', '2') // Additional cases from PR review + simplifyAndCompare('-28y/-4y', '7') + simplifyAndCompare('-28*(z/-4z)', '7') + simplifyAndCompare('(x^2 + 2x)*x', '2*x^2 + x^3') + simplifyAndCompare('x + y/z', 'x + y/z') // avoid overzealous '(x+y*z)/z' }) it('should collect separated like factors', function () { @@ -415,38 +370,6 @@ describe('simplify', function () { simplifyAndCompare('x-(y-y+x)', '0', {}, optsNAANCM) }) - it('resolve() should substitute scoped constants', function () { - assert.strictEqual( - math.simplify.resolve(math.parse('x+y'), { x: 1 }).toString(), - '1 + y' - ) // direct - simplifyAndCompare('x+y', 'x+y', {}) // operator - simplifyAndCompare('x+y', 'y+1', { x: 1 }) - simplifyAndCompare('x+y', 'y+1', { x: math.parse('1') }) - simplifyAndCompare('x+y', '3', { x: 1, y: 2 }) - simplifyAndCompare('x+x+x', '3*x') - simplifyAndCompare('y', 'x+1', { y: math.parse('1+x') }) - simplifyAndCompare('y', '3', { x: 2, y: math.parse('1+x') }) - simplifyAndCompare('x+y', '3*x', { y: math.parse('x+x') }) - simplifyAndCompare('x+y', '6', { x: 2, y: math.parse('x+x') }) - simplifyAndCompare('x+(y+2-1-1)', '6', { x: 2, y: math.parse('x+x') }) // parentheses - simplifyAndCompare('log(x+y)', String(Math.log(6)), { x: 2, y: math.parse('x+x') }) // function - simplifyAndCompare('combinations( ceil(abs(sin(x)) * (y+3)), abs(x) )', - 'combinations(ceil(0.9092974268256817 * (y + 3) ), 2)', { x: -2 }) - - // TODO(deal with accessor nodes) simplifyAndCompare('size(text)[1]', '11', {text: "hello world"}) - }) - - it('resolve() should substitute scoped constants from Map like scopes', function () { - assert.strictEqual( - math.simplify.resolve(math.parse('x+y'), new Map([['x', 1]])).toString(), - '1 + y' - ) // direct - simplifyAndCompare('x+y', 'x+y', new Map()) // operator - simplifyAndCompare('x+y', 'y+1', new Map([['x', 1]])) - simplifyAndCompare('x+y', 'y+1', new Map([['x', math.parse('1')]])) - }) - it('should keep implicit multiplication implicit', function () { const f = math.parse('2x') assert.strictEqual(f.toString({ implicit: 'hide' }), '2 x') diff --git a/test/unit-tests/function/algebra/simplifyCore.test.js b/test/unit-tests/function/algebra/simplifyCore.test.js new file mode 100644 index 000000000..e8c048be2 --- /dev/null +++ b/test/unit-tests/function/algebra/simplifyCore.test.js @@ -0,0 +1,60 @@ +// test simplifyCore +import assert from 'assert' + +import math from '../../../../src/defaultInstance.js' + +describe('simplifyCore', function () { + const testSimplifyCore = function (expr, expected, opts = {}) { + const actual = math.simplifyCore(math.parse(expr)).toString(opts) + assert.strictEqual(actual, expected) + } + + it('should handle different node types', function () { + testSimplifyCore('5*x*3', '15 * x') + testSimplifyCore('5*x*3*x', '15 * x * x') + + testSimplifyCore('x-0', 'x') + testSimplifyCore('0-x', '-x') + testSimplifyCore('0-3', '-3') + testSimplifyCore('x+0', 'x') + testSimplifyCore('0+x', 'x') + testSimplifyCore('0*x', '0') + testSimplifyCore('x*0', '0') + testSimplifyCore('x*1', 'x') + testSimplifyCore('1*x', 'x') + testSimplifyCore('-(x)', '-x') + testSimplifyCore('0/x', '0') + testSimplifyCore('(1*x + y*0)*1+0', 'x') + testSimplifyCore('sin(x+0)*1', 'sin(x)') + testSimplifyCore('((x+0)*1)', 'x') + testSimplifyCore('sin((x-0)*1+y*0)', 'sin(x)') + testSimplifyCore('[x+0,1*y,z*0]', '[x, y, 0]') + testSimplifyCore('(a+b+0)[n*0+1,-(n)]', '(a + b)[1, -n]') + testSimplifyCore('{a:x*1, b:y-0}', '{"a": x, "b": y}') + }) + + it('strips ParenthesisNodes (implicit in tree)', function () { + testSimplifyCore('((x)*(y))', 'x * y') + testSimplifyCore('((x)*(y))^1', 'x * y') + testSimplifyCore('x*(y+z)', 'x * (y + z)') + testSimplifyCore('x+(y+z)+w', 'x + y + z + w') + // But it doesn't actually change the association internally: + testSimplifyCore('x+ y+z +w', '((x + y) + z) + w', { parenthesis: 'all' }) + 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 () { + const result = math.simplify( + 'x + y + a', [math.simplifyCore], { a: -1 } + ).toString() + assert.strictEqual(result, 'x + y - 1') + }) +}) diff --git a/tools/docgenerator.js b/tools/docgenerator.js index baeb86671..f18b2cc7b 100644 --- a/tools/docgenerator.js +++ b/tools/docgenerator.js @@ -282,6 +282,34 @@ function generateDoc (name, code) { return count > 0 } + function parseThrows () { + let count = 0 + let match + do { + match = /\s*@throws\s*\{(.*)}\s*(.*)?$/.exec(line) + if (match) { + next() + + count++ + const annotation = { + description: (match[2] || '').trim(), + type: (match[1] || '').trim() + } + doc.mayThrow.push(annotation) + + // multi line description (must be non-empty and not start with @param or @return) + while (exists() && !empty() && !/^\s*@/.test(line)) { + const lineTrim = line.trim() + const separator = (lineTrim[0] === '-' ? '
' : ' ') + annotation.description += separator + lineTrim + next() + } + } + } while (match) + + return count > 0 + } + function parseReturns () { const match = /\s*@returns?\s*\{(.*)}\s*(.*)?$/.exec(line) if (match) { @@ -312,7 +340,8 @@ function generateDoc (name, code) { examples: [], seeAlso: [], parameters: [], - returns: null + returns: null, + mayThrow: [] } next() @@ -327,7 +356,8 @@ function generateDoc (name, code) { parseExamples() || parseSeeAlso() || parseParameters() || - parseReturns() + parseReturns() || + parseThrows() if (!handled) { // skip this line, no one knows what to do with it @@ -384,6 +414,15 @@ function validateDoc (doc) { } } + if (doc.mayThrow && doc.mayThrow.length) { + doc.mayThrow.forEach(function (err, index) { + if (!err.type) { + issues.push( + 'function "' + doc.name + '": error type missing for throw ' + index) + } + }) + } + if (doc.returns) { if (!doc.returns.description || !doc.returns.description.trim()) { issues.push('function "' + doc.name + '": description missing of returns') @@ -453,6 +492,16 @@ function generateMarkdown (doc, functions) { '\n\n\n' } + if (doc.mayThrow) { + text += '### Throws\n\n' + + 'Type | Description\n' + + '---- | -----------\n' + + doc.mayThrow.map(function (t) { + return (t.type || '') + ' | ' + t.description + }).join('\n') + + '\n\n' + } + if (doc.examples && doc.examples.length) { text += '## Examples\n\n' + '```js\n' + diff --git a/types/index.d.ts b/types/index.d.ts index 1be904cff..e4e4da656 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -575,7 +575,7 @@ declare namespace math { * @param unit The unit to be created * @returns The created unit */ - unit(value: number | MathArray | Matrix, unit: string): Unit; + unit(value: number | MathArray | Matrix | BigNumber, unit: string): Unit; /************************************************************************* * Expression functions @@ -1796,7 +1796,7 @@ declare namespace math { * @param n A real or complex number * @returns The gamma of n */ - gamma(n: number | MathArray | Matrix): number | MathArray | Matrix; + gamma(n: T): NoLiteralType; /** * Calculate the Kullback-Leibler (KL) divergence between two @@ -3134,6 +3134,28 @@ declare namespace math { fixPrefix?: boolean; } + interface UnitComponent { + power: number; + prefix: string; + unit: { + name: string; + base: { + dimensions: number[]; + key: string; + }; + prefixes: Record; + value: number; + offset: number; + dimensions: number[]; + }; + } + + interface UnitPrefix { + name: string; + value: number; + scientific: boolean; + } + interface Unit { valueOf(): string; clone(): Unit; @@ -3152,7 +3174,14 @@ declare namespace math { toJSON(): MathJSON; formatUnits(): string; format(options: FormatOptions): string; + simplify(): Unit; splitUnit(parts: ReadonlyArray): Unit[]; + + units: UnitComponent[]; + dimensions: number[]; + value: number; + fixPrefix: boolean; + skipAutomaticSimplification: true; } interface CreateUnitOptions {