From 1b34c580f6c2407feeb3cdb00b0b6028ec902656 Mon Sep 17 00:00:00 2001 From: Josh Hansen Date: Sun, 27 Feb 2022 23:45:47 -0800 Subject: [PATCH 1/4] fix(types): `Unit` Typescript type update (#2450) Add missing BigNumber parameter type to `math.unit` and add types for Unit.simplify and Unit.units Resolves #2353. --- HISTORY.md | 2 ++ types/index.d.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) 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/types/index.d.ts b/types/index.d.ts index 1be904cff..ea03783ac 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 @@ -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 { From 15c4254bc92a5cf40dae01f7c18afa30212b8468 Mon Sep 17 00:00:00 2001 From: yifanwww Date: Mon, 28 Feb 2022 16:15:25 +0800 Subject: [PATCH 2/4] feat: Make `gamma` generic (#2416) Co-authored-by: Jos de Jong --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index ea03783ac..e4e4da656 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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 From 151926c75b7c37baa554cba61403ceb3564e588a Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Feb 2022 00:58:10 -0800 Subject: [PATCH 3/4] fix: simplify.resolve detects reference loop and throws error (#2405) * docs: Enhance generation to pick up functions with a prefix For example, prior to this commit, docgenerator.js would miss simplify.resolve because it is not a direct key of the math object. Also incorporates any "throws" attributes in the comments into the generated documentation, and uses this to document the new error-case behavior of simplify.resolve to be added in the next commit. * fix(resolve): Detect and throw errors for reference loops Also extends resolve to work inside all node types. Adds tests for both changes. * docs: Add embedded docs for simplify.resolve et al. To support finding the embedded doc from the `math.simplify.resolve` function itself, required extending the search for objects with documentation one level deeper in the `help()` function. Added test for this search. Also added support for documenting throws in embedded docs. * refactor(simplify): Move resolve and simplifyCore to top-level Also reverts changes searching for docs and embedded docs one level down in the naming hierarchy. Also splits tests for resolve and simplifyCore into their own files, reflecting their new top-level status. * fix(resolve): Remaining changes as requested Also removed a stray blank line inadvertently introduced in docgenerator.js * refactor: Declare resolve and simplifyCore as dependencies of simplify ... rather than explicitly loading them, which is unnecessary now that they are at top level. * fix: Add dependencies to factoriesNumber Also register simplifyCore as a dependency to rationalize Co-authored-by: Jos de Jong --- src/expression/Help.js | 3 + src/expression/embeddedDocs/embeddedDocs.js | 4 + .../embeddedDocs/function/algebra/resolve.js | 20 +++ .../embeddedDocs/function/algebra/simplify.js | 2 +- .../function/algebra/simplifyCore.js | 15 ++ src/factoriesAny.js | 2 + src/factoriesNumber.js | 2 + src/function/algebra/rationalize.js | 21 +-- src/function/algebra/resolve.js | 94 ++++++++++ src/function/algebra/simplify.js | 34 +--- src/function/algebra/simplify/resolve.js | 67 -------- .../algebra/{simplify => }/simplifyCore.js | 15 +- .../function/algebra/resolve.test.js | 64 +++++++ .../function/algebra/simplify.test.js | 162 +++++------------- .../function/algebra/simplifyCore.test.js | 60 +++++++ tools/docgenerator.js | 53 +++++- 16 files changed, 370 insertions(+), 248 deletions(-) create mode 100644 src/expression/embeddedDocs/function/algebra/resolve.js create mode 100644 src/expression/embeddedDocs/function/algebra/simplifyCore.js create mode 100644 src/function/algebra/resolve.js delete mode 100644 src/function/algebra/simplify/resolve.js rename src/function/algebra/{simplify => }/simplifyCore.js (94%) create mode 100644 test/unit-tests/function/algebra/resolve.test.js create mode 100644 test/unit-tests/function/algebra/simplifyCore.test.js 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..cfe3c0c5f 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 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..d5f706e0c 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 * }) @@ -415,38 +363,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' + From 1ca208cd6dbd4980d050e55925eff83ade67ee56 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 28 Feb 2022 02:05:07 -0800 Subject: [PATCH 4/4] fix(simplify): Collect like factors, and cancel like terms, in sums (#2388) * fix(simplify): Collect like factors in sums and cancel like terms in sums Since polynomials like `x*(2x+x^2)` are usually written out as polynomials `2x^2+x^3`, adds rules to be more eager to move factors into sums to collect like factors. To complement this, adds a rule extracting negative powers from a sum. Together, these accomplish canceling a common factor in numerator and denominator: (a*k + b*k^2)/k^4 -> k^-4*(a*k + b*k^2) -> a*k^-3 + b*k^-2 -> k^-3*(a + k*b) -> (a + k*b)/k^3 Resolves #1423. * fix(simplify): Adjust for #2394 * chore: Rebase and mark rules with assumptions This commit should update this PR to be fully compatible with the current development mainline. Will remove 'WIP'. * chore(simplify): Additional test cases from PR review Co-authored-by: Jos de Jong --- src/function/algebra/simplify.js | 41 ++++++++++++++++--- .../function/algebra/simplify.test.js | 7 ++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index cfe3c0c5f..4c9ac5c0c 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -350,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' }, @@ -363,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)', @@ -382,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', @@ -390,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 } } @@ -398,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)', @@ -438,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/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index d5f706e0c..4bbf55e5d 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -249,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 () {