diff --git a/examples/symbolic_evaluation.mjs b/examples/symbolic_evaluation.mjs new file mode 100644 index 0000000000..1ef1c037d1 --- /dev/null +++ b/examples/symbolic_evaluation.mjs @@ -0,0 +1,122 @@ +import math from '../src/defaultInstance.js' + +math.SymbolNode.onUndefinedSymbol = (name, node) => node + +math.typed.onMismatch = (name, args, signatures) => { + let nodeArg = false + for (const arg of args) { + if (math.isNode(arg)) { + nodeArg = true + break + } + } + if (nodeArg) { + const specialOps = { addScalar: 'add', multiplyScalar: 'multiply' } + if (name in specialOps) name = specialOps[name] + const maybeOp = math.OperatorNode.getOperator(name) + const newArgs = Array.from(args, arg => math.simplify.ensureNode(arg)) + if (maybeOp) return new math.OperatorNode(maybeOp, name, newArgs) + return new math.FunctionNode(new math.SymbolNode(name), newArgs) + } + + let argstr = args[0].toString() + for (let i = 1; i < args.length; ++i) { + argstr += `, ${args[i]}` + } + + throw TypeError(`Typed function type mismatch for ${name} called with '${argstr}'`) +} + +function mystringify (obj) { + let s = '{' + for (const key in obj) { + s += `${key}: ${obj[key]}, ` + } + return s.slice(0, -2) + '}' +} + +function logExample (expr, scope = {}) { + let header = `Evaluating: '${expr}'` + if (Object.keys(scope).length > 0) { + header += ` in scope ${mystringify(scope)}` + } + console.log(header) + let result + try { + result = math.evaluate(expr, scope) + if (math.isNode(result)) { + result = `Expression ${result.toString()}` + } + } catch (err) { + result = err.toString() + } + console.log(` --> ${result}`) +} + +let point = 1 +console.log(`${point++}. By just evaluating all unknown symbols to themselves, and +providing a typed-function handler that builds expression trees when there is +no matching signature, we implement full-fledged symbolic evaluation:`) +logExample('x*y + 3x - y + 2', { y: 7 }) +console.log(` +${point++}. If all of the free variables have values, this evaluates +all the way to the numeric value:`) +logExample('x*y + 3x - y + 2', { x: 1, y: 7 }) +console.log(` +${point++}. It works with matrices as well, for example.`) +logExample('[x^2 + 3x + x*y, y, 12]', { x: 2 }) +logExample('[x^2 + 3x + x*y, y, 12]', { x: 2, y: 7 }) +console.log(`(Note there are no fractions as in the simplifyConstant +version, since we are using ordinary 'math.evaluate()' in this approach.) + +${point++}. However, to break a chain of automatic conversions that disrupts +this style of evaluation, it's necessary to remove the former conversion +from 'number' to 'string':`) +logExample('count(57)') +console.log(`(In develop, this returns 2, the length of the string representation +of 57. However, it turns out that with only very slight tweaks to "Unit," +all tests pass without the automatic 'number' -> 'string' conversion, +suggesting it isn't really being used, or at least very little. + +${point++}. This lets you more easily perform operations like symbolic differentiation:`) +logExample('derivative(sin(x) + exp(x) + x^3, x)') +console.log("(Note no quotes in the argument to 'derivative' -- it is directly\n" + + 'operating on the expression, without any string values involved.)') + +console.log(` +${point++}. Doing it this way respects assignment, since ordinary evaluate does:`) +logExample('f = x^2+2x*y; derivative(f,x)') +console.log(` +${point++}. You can also build up expressions incrementally and use the scope:`) +logExample('h1 = x^2+5x; h3 = h1 + h2; derivative(h3,x)', { + h2: math.evaluate('3x+7') +}) +console.log(` +${point++}. Some kinks still remain at the moment. Scope values for the +variable of differentiation disrupt the results:`) +logExample('derivative(x^3 + x^2, x)') +logExample('derivative(x^3 + x^2, x)', { x: 1 }) +console.log(`${''}(We'd like the latter evaluation to return the result of the +first differentiation, evaluated at 1, or namely 5. However, there is not (yet) +a concept in math.evaluate that 'derivative' creates a variable-binding +environment, blocking off the 'x' from being substituted via the outside +scope within its first argument. Implementing this may be slightly trickier +in this approach since ordinary 'evaluate' (in the absence of 'rawArgs' +markings) is an essentially "bottom-up" operation whereas 'math.resolve' is +more naturally a "top-down" operation. The point is you need to know you're +inside a 'derivative' or other binding environment at the time that you do +substitution.) + +Also, unlike the simplifyConstant approach, derivative doesn't know to +'check' whether a contained variable actually depends on 'x', so the order +of assignments makes a big difference:`) +logExample('h3 = h1+h2; h1 = x^2+5x; derivative(h3,x)', { + h2: math.evaluate('3x+7') +}) +console.log(`${''}(Here, 'h1' in the first assignment evaluates to a +SymbolNode('h1'), which ends up being part of the argument to the eventual +derivative call, and there's never anything to fill in the later definition +of 'h1', and as it's a different symbol, its derivative with respect to 'x' +is assumed to be 0.) + +Nevertheless, such features could be implemented.`) diff --git a/src/core/function/typed.js b/src/core/function/typed.js index ad51b4a643..ca60622cb8 100644 --- a/src/core/function/typed.js +++ b/src/core/function/typed.js @@ -179,13 +179,13 @@ export const createTyped = /* #__PURE__ */ factory('typed', dependencies, functi return new Complex(x, 0) } - }, { - from: 'number', - to: 'string', - convert: function (x) { - return x + '' - } - }, { + }, // { + // from: 'number', + // to: 'string', + // convert: function (x) { + // return x + '' + // } + /* }, */ { from: 'BigNumber', to: 'Complex', convert: function (x) { diff --git a/src/expression/node/OperatorNode.js b/src/expression/node/OperatorNode.js index 5dc5c75df4..76b16441f2 100644 --- a/src/expression/node/OperatorNode.js +++ b/src/expression/node/OperatorNode.js @@ -2,7 +2,7 @@ import { isNode } from '../../utils/is.js' import { map } from '../../utils/array.js' import { escape } from '../../utils/string.js' import { getSafeProperty, isSafeMethod } from '../../utils/customs.js' -import { getAssociativity, getPrecedence, isAssociativeWith, properties } from '../operators.js' +import { getAssociativity, getPrecedence, getOperator, isAssociativeWith, properties } from '../operators.js' import { latexOperators } from '../../utils/latex.js' import { factory } from '../../utils/factory.js' @@ -613,5 +613,7 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({ return this.type + ':' + this.fn } + OperatorNode.getOperator = getOperator + return OperatorNode }, { isClass: true, isNode: true }) diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index 57b4c098f0..5f2c513781 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -74,13 +74,13 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m } } else { const isUnit = isValuelessUnit(name) - + const me = this return function (scope, args, context) { return scope.has(name) ? scope.get(name) : isUnit ? new Unit(null, name) - : SymbolNode.onUndefinedSymbol(name) + : SymbolNode.onUndefinedSymbol(name, me) } } } diff --git a/src/expression/operators.js b/src/expression/operators.js index 4a3e380486..049715f3c1 100644 --- a/src/expression/operators.js +++ b/src/expression/operators.js @@ -34,6 +34,7 @@ export const properties = [ }, { // logical or 'OperatorNode:or': { + op: 'or', associativity: 'left', associativeWith: [] } @@ -41,56 +42,67 @@ export const properties = [ }, { // logical xor 'OperatorNode:xor': { + op: 'xor', associativity: 'left', associativeWith: [] } }, { // logical and 'OperatorNode:and': { + op: 'and', associativity: 'left', associativeWith: [] } }, { // bitwise or 'OperatorNode:bitOr': { + op: '|', associativity: 'left', associativeWith: [] } }, { // bitwise xor 'OperatorNode:bitXor': { + op: '^|', associativity: 'left', associativeWith: [] } }, { // bitwise and 'OperatorNode:bitAnd': { + op: '&', associativity: 'left', associativeWith: [] } }, { // relational operators 'OperatorNode:equal': { + op: '==', associativity: 'left', associativeWith: [] }, 'OperatorNode:unequal': { + op: '!=', associativity: 'left', associativeWith: [] }, 'OperatorNode:smaller': { + op: '<', associativity: 'left', associativeWith: [] }, 'OperatorNode:larger': { + op: '<', associativity: 'left', associativeWith: [] }, 'OperatorNode:smallerEq': { + op: '<=', associativity: 'left', associativeWith: [] }, 'OperatorNode:largerEq': { + op: '>=', associativity: 'left', associativeWith: [] }, @@ -101,20 +113,24 @@ export const properties = [ }, { // bitshift operators 'OperatorNode:leftShift': { + op: '<<', associativity: 'left', associativeWith: [] }, 'OperatorNode:rightArithShift': { + op: '>>', associativity: 'left', associativeWith: [] }, 'OperatorNode:rightLogShift': { + op: '>>>', associativity: 'left', associativeWith: [] } }, { // unit conversion 'OperatorNode:to': { + op: 'to', associativity: 'left', associativeWith: [] } @@ -124,16 +140,19 @@ export const properties = [ }, { // addition, subtraction 'OperatorNode:add': { + op: '+', associativity: 'left', associativeWith: ['OperatorNode:add', 'OperatorNode:subtract'] }, 'OperatorNode:subtract': { + op: '-', associativity: 'left', associativeWith: [] } }, { // multiply, divide, modulus 'OperatorNode:multiply': { + op: '*', associativity: 'left', associativeWith: [ 'OperatorNode:multiply', @@ -143,6 +162,7 @@ export const properties = [ ] }, 'OperatorNode:divide': { + op: '/', associativity: 'left', associativeWith: [], latexLeftParens: false, @@ -153,6 +173,7 @@ export const properties = [ // in LaTeX }, 'OperatorNode:dotMultiply': { + op: '.*', associativity: 'left', associativeWith: [ 'OperatorNode:multiply', @@ -162,30 +183,37 @@ export const properties = [ ] }, 'OperatorNode:dotDivide': { + op: './', associativity: 'left', associativeWith: [] }, 'OperatorNode:mod': { + op: 'mod', associativity: 'left', associativeWith: [] } }, { // unary prefix operators 'OperatorNode:unaryPlus': { + op: '+', associativity: 'right' }, 'OperatorNode:unaryMinus': { + op: '-', associativity: 'right' }, 'OperatorNode:bitNot': { + op: '~', associativity: 'right' }, 'OperatorNode:not': { + op: 'not', associativity: 'right' } }, { // exponentiation 'OperatorNode:pow': { + op: '^', associativity: 'right', associativeWith: [], latexRightParens: false @@ -194,17 +222,20 @@ export const properties = [ // (it's on top) }, 'OperatorNode:dotPow': { + op: '.^', associativity: 'right', associativeWith: [] } }, { // factorial 'OperatorNode:factorial': { + op: '!', associativity: 'left' } }, { // matrix transpose - 'OperatorNode:transpose': { + 'OperatorNode:ctranspose': { + op: "'", associativity: 'left' } } @@ -309,3 +340,22 @@ export function isAssociativeWith (nodeA, nodeB, parenthesis) { // associativeWith is not defined return null } + +/** + * Get the operator associated with a function name. + * Returns a string with the operator symbol, or null if the + * input is not the name of a function associated with an + * operator. + * + * @param {string} Function name + * @return {string | null} Associated operator symbol, if any + */ +export function getOperator (fn) { + const identifier = 'OperatorNode:' + fn + for (const group of properties) { + if (identifier in group) { + return group[identifier].op + } + } + return null +} diff --git a/src/function/algebra/simplify.js b/src/function/algebra/simplify.js index ee18b2eeed..cf79f4181a 100644 --- a/src/function/algebra/simplify.js +++ b/src/function/algebra/simplify.js @@ -279,6 +279,7 @@ export const createSimplify = /* #__PURE__ */ factory(name, dependencies, ( simplify.defaultContext = defaultContext simplify.realContext = realContext simplify.positiveContext = positiveContext + simplify.ensureNode = simplifyConstant.ensureNode function removeParens (node) { return node.transform(function (node, path, parent) { diff --git a/src/function/algebra/simplify/simplifyConstant.js b/src/function/algebra/simplify/simplifyConstant.js index 577b0f8c32..86d54c8165 100644 --- a/src/function/algebra/simplify/simplifyConstant.js +++ b/src/function/algebra/simplify/simplifyConstant.js @@ -428,5 +428,7 @@ export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies } } + simplifyConstant.ensureNode = _ensureNode + return simplifyConstant }) diff --git a/src/function/algebra/simplifyCore.js b/src/function/algebra/simplifyCore.js index ffbdf3aa61..59e9534ae9 100644 --- a/src/function/algebra/simplifyCore.js +++ b/src/function/algebra/simplifyCore.js @@ -1,9 +1,12 @@ import { isAccessorNode, isArrayNode, isConstantNode, isFunctionNode, isIndexNode, isObjectNode, isOperatorNode } from '../../utils/is.js' +import { getOperator } from '../../expression/operators.js' import { createUtil } from './simplify/util.js' import { factory } from '../../utils/factory.js' const name = 'simplifyCore' const dependencies = [ + 'typed', + 'parse', 'equal', 'isZero', 'add', @@ -23,6 +26,8 @@ const dependencies = [ ] export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ + typed, + parse, equal, isZero, add, @@ -71,164 +76,199 @@ export const createSimplifyCore = /* #__PURE__ */ factory(name, dependencies, ({ * Simplification options, as per simplify() * @return {Node} Returns expression with basic simplifications applied */ - function simplifyCore (node, options) { - const context = options ? options.context : undefined - if (hasProperty(node, 'trivial', context)) { - // This node does nothing if it has only one argument, so if so, - // return that argument simplified - if (isFunctionNode(node) && node.args.length === 1) { - return simplifyCore(node.args[0], options) - } - // For other node types, we try the generic methods - let simpChild = false - let childCount = 0 - node.forEach(c => { - ++childCount + const simplifyCore = typed('simplifyCore', { + 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 (nodeToSimplify, options) { + const context = options ? options.context : undefined + if (hasProperty(nodeToSimplify, 'trivial', context)) { + // This node does nothing if it has only one argument, so if so, + // return that argument simplified + if (isFunctionNode(nodeToSimplify) && nodeToSimplify.args.length === 1) { + return simplifyCore(nodeToSimplify.args[0], options) + } + // For other node types, we try the generic methods + let simpChild = false + let childCount = 0 + nodeToSimplify.forEach(c => { + ++childCount + if (childCount === 1) { + simpChild = simplifyCore(c, options) + } + }) if (childCount === 1) { - simpChild = simplifyCore(c, options) + return simpChild } - }) - if (childCount === 1) { - return simpChild } - } - if (isOperatorNode(node) && node.isUnary()) { - const a0 = simplifyCore(node.args[0], options) - - if (node.op === '-') { // unary minus - if (isOperatorNode(a0)) { - 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]]) + let node = nodeToSimplify + if (isFunctionNode(node)) { + const op = getOperator(node.name) + if (op) { + // Replace FunctionNode with a new OperatorNode + if (node.args.length > 2 && hasProperty(node, 'associative', context)) { + // unflatten into binary operations since that's what simplifyCore handles + while (node.args.length > 2) { + const last = node.args.pop() + const seclast = node.args.pop() + node.args.push(new OperatorNode(op, node.name, [last, seclast])) + } } + node = new OperatorNode(op, node.name, node.args) + } else { + return new FunctionNode( + simplifyCore(node.fn), node.args.map(n => simplifyCore(n, options))) } - return new OperatorNode(node.op, node.fn, [a0]) } - } else if (isOperatorNode(node) && node.isBinary()) { - const a0 = simplifyCore(node.args[0], options) - const a1 = simplifyCore(node.args[1], options) + if (isOperatorNode(node) && node.isUnary()) { + const a0 = simplifyCore(node.args[0], 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 (node.op === '-') { // unary minus + if (isOperatorNode(a0)) { + 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]) } - if (isConstantNode(a1) && isZero(a1.value)) { - return a0 - } - if (isOperatorNode(a1) && a1.isUnary() && a1.op === '-') { - return new OperatorNode('-', 'subtract', [a0, a1.args[0]]) - } - 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]) + } else if (isOperatorNode(node) && node.isBinary()) { + const a0 = simplifyCore(node.args[0], options) + const 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 (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('-', 'subtract', [a0, a1.args[0]]) } - return new OperatorNode(node.op, node.fn, [a0, a1]) - } - } else 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)) + 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 (isConstantNode(a1)) { - if (isZero(a1.value)) { - 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 (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 (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) + } else 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)) + } } - } - 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 (isConstantNode(a1)) { + if (isZero(a1.value)) { + 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]) - } else if (node.op === '^') { - if (isConstantNode(a1)) { - if (isZero(a1.value)) { - return node1 - } else if (equal(a1.value, 1)) { - return a0 - } 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 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)) + } + } + return new OperatorNode(node.op, node.fn, [a0, a1]) + } else if (node.op === '^') { + if (isConstantNode(a1)) { + if (isZero(a1.value)) { + return node1 + } else if (equal(a1.value, 1)) { + return a0 + } 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 new OperatorNode(node.op, node.fn, [a0, a1]) + } else if (isOperatorNode(node)) { + return new OperatorNode(node.op, node.fn, + node.args.map(a => simplifyCore(a, options))) + } + if (isArrayNode(node)) { + return new ArrayNode(node.items.map(n => simplifyCore(n, options))) + } + if (isAccessorNode(node)) { + return new AccessorNode( + simplifyCore(node.object, options), simplifyCore(node.index, options)) } - return new OperatorNode(node.op, node.fn, [a0, a1]) - } else if (isFunctionNode(node)) { - return new FunctionNode( - simplifyCore(node.fn), node.args.map(n => simplifyCore(n, options))) - } else if (isArrayNode(node)) { - return new ArrayNode( - node.items.map(n => simplifyCore(n, options))) - } else if (isAccessorNode(node)) { - return new AccessorNode( - simplifyCore(node.object, options), simplifyCore(node.index, options)) - } else if (isIndexNode(node)) { - return new IndexNode( - node.dimensions.map(n => simplifyCore(n, options))) - } else if (isObjectNode(node)) { - const newProps = {} - for (const prop in node.properties) { - newProps[prop] = simplifyCore(node.properties[prop], options) + if (isIndexNode(node)) { + return new IndexNode( + node.dimensions.map(n => simplifyCore(n, options))) + } + if (isObjectNode(node)) { + const newProps = {} + for (const prop in node.properties) { + newProps[prop] = simplifyCore(node.properties[prop], options) + } + return new ObjectNode(newProps) } - return new ObjectNode(newProps) - } else { // cannot simplify + return node } - return node - } + }) return simplifyCore }) diff --git a/src/type/unit/Unit.js b/src/type/unit/Unit.js index 7c4ade2ead..1b6ef0f151 100644 --- a/src/type/unit/Unit.js +++ b/src/type/unit/Unit.js @@ -82,13 +82,7 @@ export const createUnitClass = /* #__PURE__ */ factory(name, dependencies, ({ this.units = u.units this.dimensions = u.dimensions } else { - this.units = [ - { - unit: UNIT_NONE, - prefix: PREFIXES.NONE, // link to a list with supported prefixes - power: 0 - } - ] + this.units = [] this.dimensions = [] for (let i = 0; i < BASE_DIMENSIONS.length; i++) { this.dimensions[i] = 0 diff --git a/src/type/unit/function/unit.js b/src/type/unit/function/unit.js index 108e643329..96c736e783 100644 --- a/src/type/unit/function/unit.js +++ b/src/type/unit/function/unit.js @@ -47,6 +47,11 @@ export const createUnitFunction = /* #__PURE__ */ factory(name, dependencies, ({ return new Unit(value, unit) }, + 'number | BigNumber | Fraction': function (value) { + // dimensionless + return new Unit(value) + }, + 'Array | Matrix': function (x) { return deepMap(x, this) } diff --git a/test/unit-tests/expression/operators.test.js b/test/unit-tests/expression/operators.test.js index 2ef9640caa..9127df0dff 100644 --- a/test/unit-tests/expression/operators.test.js +++ b/test/unit-tests/expression/operators.test.js @@ -1,6 +1,6 @@ import assert from 'assert' import math from '../../../src/defaultInstance.js' -import { getAssociativity, getPrecedence, isAssociativeWith } from '../../../src/expression/operators.js' +import { getAssociativity, getPrecedence, isAssociativeWith, getOperator } from '../../../src/expression/operators.js' const OperatorNode = math.OperatorNode const AssignmentNode = math.AssignmentNode const SymbolNode = math.SymbolNode @@ -15,9 +15,11 @@ describe('operators', function () { const n1 = new AssignmentNode(new SymbolNode('a'), a) const n2 = new OperatorNode('or', 'or', [a, b]) + const n3 = math.parse("M'") assert.strictEqual(getPrecedence(n1, 'keep'), 0) assert.strictEqual(getPrecedence(n2, 'keep'), 2) + assert.strictEqual(getPrecedence(n3, 'keep'), 17) }) it('should return null if precedence is not defined for a node', function () { @@ -45,11 +47,13 @@ describe('operators', function () { const n2 = new OperatorNode('^', 'pow', [a, a]) const n3 = new OperatorNode('-', 'unaryMinus', [a]) const n4 = new OperatorNode('!', 'factorial', [a]) + const n5 = math.parse("M'") assert.strictEqual(getAssociativity(n1, 'keep'), 'left') assert.strictEqual(getAssociativity(n2, 'keep'), 'right') assert.strictEqual(getAssociativity(n3, 'keep'), 'right') assert.strictEqual(getAssociativity(n4, 'keep'), 'left') + assert.strictEqual(getAssociativity(n5, 'keep'), 'left') }) it('should return the associativity of a ParenthesisNode', function () { @@ -110,4 +114,11 @@ describe('operators', function () { assert.strictEqual(isAssociativeWith(p, sub, 'auto'), true) assert.strictEqual(isAssociativeWith(p, sub, 'keep'), null) }) + + it('should get the operator of a function name', () => { + assert.strictEqual(getOperator('multiply'), '*') + assert.strictEqual(getOperator('ctranspose'), "'") + assert.strictEqual(getOperator('mod'), 'mod') + assert.strictEqual(getOperator('square'), null) + }) }) diff --git a/test/unit-tests/function/algebra/derivative.test.js b/test/unit-tests/function/algebra/derivative.test.js index d061f84717..c940f68430 100644 --- a/test/unit-tests/function/algebra/derivative.test.js +++ b/test/unit-tests/function/algebra/derivative.test.js @@ -254,7 +254,7 @@ describe('derivative', function () { it('should throw error for incorrect argument types', function () { assert.throws(function () { derivative('42', '42') - }, /TypeError: Unexpected type of argument in function derivative \(expected: string or SymbolNode or number or boolean, actual: ConstantNode, index: 1\)/) + }, /TypeError: Unexpected type of argument in function derivative \(expected: string or SymbolNode or boolean, actual: ConstantNode, index: 1\)/) assert.throws(function () { derivative('[1, 2; 3, 4]', 'x') @@ -268,7 +268,7 @@ describe('derivative', function () { it('should throw error if incorrect number of arguments', function () { assert.throws(function () { derivative('x + 2') - }, /TypeError: Too few arguments in function derivative \(expected: string or SymbolNode or number or boolean, index: 1\)/) + }, /TypeError: Too few arguments in function derivative \(expected: string or SymbolNode or boolean, index: 1\)/) assert.throws(function () { derivative('x + 2', 'x', {}, true, 42) diff --git a/test/unit-tests/function/algebra/simplify.test.js b/test/unit-tests/function/algebra/simplify.test.js index 607c8ee6b9..dc09c26a67 100644 --- a/test/unit-tests/function/algebra/simplify.test.js +++ b/test/unit-tests/function/algebra/simplify.test.js @@ -233,6 +233,19 @@ describe('simplify', function () { simplifyAndCompare('x^2*y^3*z - y*z*x^2*y', 'x^2*z*(y^3-y^2)') }) + it('can simplify with functions as well as operators', () => { + simplifyAndCompare('add(x,x)', '2*x') + simplifyAndCompare('multiply(x,2)+x', '3*x') + simplifyAndCompare('add(2*add(x,1), x+1)', '3*(x + 1)') + simplifyAndCompare('multiply(2, x+1) + add(x,1)', '3*(x + 1)') + simplifyAndCompare('add(y*pow(x,2), multiply(2,x^2))', 'x^2*(y+2)') + simplifyAndCompare('add(x*y, multiply(y,x))', '2*x*y') + simplifyAndCompare('subtract(multiply(x,y), multiply(y,x))', '0') + simplifyAndCompare('pow(x,2)*multiply(y^3, z) - multiply(y,z,y,x^2,y)', '0') + simplifyAndCompare('subtract(multiply(x^2, pow(y,3))*z, y*multiply(z,x^2)*y)', + 'x^2*z*(y^3-y^2)') + }) + it('should collect separated like terms', function () { simplifyAndCompare('x+1+x', '2*x+1') simplifyAndCompare('x^2+x+3+x^2', '2*x^2+x+3') diff --git a/test/unit-tests/function/algebra/simplifyCore.test.js b/test/unit-tests/function/algebra/simplifyCore.test.js index 362b1c1a36..2bd0666050 100644 --- a/test/unit-tests/function/algebra/simplifyCore.test.js +++ b/test/unit-tests/function/algebra/simplifyCore.test.js @@ -4,8 +4,10 @@ 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) + const testSimplifyCore = function (expr, expected, opts = {}, simpOpts = {}) { + let actual = math.simplifyCore(math.parse(expr), simpOpts).toString(opts) + assert.strictEqual(actual, expected) + actual = math.simplifyCore(expr, simpOpts).toString(opts) assert.strictEqual(actual, expected) } @@ -33,6 +35,14 @@ describe('simplifyCore', function () { testSimplifyCore('{a:x*1, b:y-0}', '{"a": x, "b": y}') }) + it('should not alter order of multiplication when noncommutative', function () { + testSimplifyCore('5*x*3', '5 * x * 3', {}, { context: { multiply: { commutative: false } } }) + }) + + it('should remove any trivial function', function () { + testSimplifyCore('foo(y)', 'y', {}, { context: { foo: { trivial: true } } }) + }) + it('strips ParenthesisNodes (implicit in tree)', function () { testSimplifyCore('((x)*(y))', 'x * y') testSimplifyCore('((x)*(y))^1', 'x * y') @@ -62,4 +72,17 @@ describe('simplifyCore', function () { testSimplifyCore('x+0==5', 'x == 5') testSimplifyCore('(x*1) % (y^1)', 'x % y') }) + + it('converts functions to their corresponding infix operators', () => { + testSimplifyCore('add(x, y)', 'x + y') + testSimplifyCore('mod(x, 5)', 'x mod 5') + testSimplifyCore('to(5 cm, in)', '5 cm to in') + testSimplifyCore('ctranspose(M)', "M'") + }) + + it('continues to simplify after function -> operator conversion', () => { + testSimplifyCore('add(multiply(x, 0), y)', 'y') + testSimplifyCore('and(multiply(1, x), true)', 'x and true') + testSimplifyCore('add(x, 0 ,y)', 'x + y') + }) }) diff --git a/test/unit-tests/function/matrix/count.test.js b/test/unit-tests/function/matrix/count.test.js index d4954b63ce..4ca323877b 100644 --- a/test/unit-tests/function/matrix/count.test.js +++ b/test/unit-tests/function/matrix/count.test.js @@ -31,7 +31,7 @@ describe('count', function () { it('should throw an error if called with an invalid number of arguments', function () { assert.throws(function () { count() }, /TypeError: Too few arguments/) - assert.throws(function () { count(1, 2) }, /TypeError: Too many arguments/) + assert.throws(function () { count('1', 2) }, /TypeError: Too many arguments/) }) it('should throw an error if called with invalid type of arguments', function () { diff --git a/test/unit-tests/function/matrix/diag.test.js b/test/unit-tests/function/matrix/diag.test.js index 68a514475b..2b1bfbc19a 100644 --- a/test/unit-tests/function/matrix/diag.test.js +++ b/test/unit-tests/function/matrix/diag.test.js @@ -108,7 +108,7 @@ describe('diag', function () { it('should throw an error in case of wrong number of arguments', function () { assert.throws(function () { math.diag() }, /TypeError: Too few arguments/) - assert.throws(function () { math.diag([], 2, 3, 4) }, /TypeError: Too many arguments/) + assert.throws(function () { math.diag([], 3, 'dense', 4) }, /TypeError: Too many arguments/) }) it('should throw an error in case of invalid type of arguments', function () { diff --git a/test/unit-tests/function/unit/to.test.js b/test/unit-tests/function/unit/to.test.js index 1da0cc4655..184950438c 100644 --- a/test/unit-tests/function/unit/to.test.js +++ b/test/unit-tests/function/unit/to.test.js @@ -70,7 +70,7 @@ describe('to', function () { it('should throw an error if called with a number', function () { assert.throws(function () { math.to(5, unit('m')) }, TypeError) - assert.throws(function () { math.to(unit('5cm'), 2) }, /SyntaxError: "2" contains no units/) + assert.throws(function () { math.to(unit('5cm'), 2) }, TypeError) }) it('should throw an error if called with a string', function () { diff --git a/test/unit-tests/type/matrix/function/matrix.test.js b/test/unit-tests/type/matrix/function/matrix.test.js index f84e50e720..285e908588 100644 --- a/test/unit-tests/type/matrix/function/matrix.test.js +++ b/test/unit-tests/type/matrix/function/matrix.test.js @@ -85,11 +85,11 @@ describe('matrix', function () { }) it('should throw an error if called with too many arguments', function () { - assert.throws(function () { matrix([], 3, 3, 7) }, /TypeError: Too many arguments/) + assert.throws(function () { matrix([], 'dense', 'number', 7) }, /TypeError: Too many arguments/) }) it('should throw an error when called with an invalid storage format', function () { - assert.throws(function () { math.matrix([], 1) }, /TypeError: Unknown matrix type "1"/) + assert.throws(function () { math.matrix([], '1') }, /TypeError: Unknown matrix type "1"/) }) it('should throw an error when called with an unknown storage format', function () { diff --git a/test/unit-tests/type/matrix/function/sparse.test.js b/test/unit-tests/type/matrix/function/sparse.test.js index 8c18b64570..88648cf3c6 100644 --- a/test/unit-tests/type/matrix/function/sparse.test.js +++ b/test/unit-tests/type/matrix/function/sparse.test.js @@ -40,7 +40,7 @@ describe('sparse', function () { }) it('should throw an error if called with too many arguments', function () { - assert.throws(function () { sparse([], 3, 3) }, /TypeError: Too many arguments/) + assert.throws(function () { sparse([], 'number', 3) }, /TypeError: Too many arguments/) }) it('should LaTeX matrix', function () {