diff --git a/src/expression/node/ConstantNode.js b/src/expression/node/ConstantNode.js index 7ab4443a22..45ca3618c4 100644 --- a/src/expression/node/ConstantNode.js +++ b/src/expression/node/ConstantNode.js @@ -19,12 +19,14 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ * new ConstantNode('hello') * * @param {*} value Value can be any type (number, BigNumber, bigint, string, ...) + * @param {number[]} range Start and end index of the parsed value in the original string * @constructor ConstantNode * @extends {Node} */ - constructor (value) { + constructor (value, range) { super() this.value = value + this.range = range } static name = name @@ -75,7 +77,7 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ * @return {ConstantNode} */ clone () { - return new ConstantNode(this.value) + return new ConstantNode(this.value, this.range) } /** @@ -120,18 +122,22 @@ export const createConstantNode = /* #__PURE__ */ factory(name, dependencies, ({ * @returns {Object} */ toJSON () { - return { mathjs: name, value: this.value } + return { + mathjs: name, + value: this.value, + ...(this.range && { range: this.range }) + } } /** * Instantiate a ConstantNode from its JSON representation * @param {Object} json An object structured like - * `{"mathjs": "SymbolNode", value: 2.3}`, + * `{"mathjs": "SymbolNode", value: 2.3, range: [0, 3]}`, * where mathjs is optional * @returns {ConstantNode} */ static fromJSON (json) { - return new ConstantNode(json.value) + return new ConstantNode(json.value, json.range) } /** diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index 36391c751e..a7022582ec 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -26,9 +26,10 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * @extends {Node} * A symbol node can hold and resolve a symbol * @param {string} name + * @param {number[]} range Start and end index of the parsed value in the original string * @extends {Node} */ - constructor (name) { + constructor (name, range) { super() // validate input if (typeof name !== 'string') { @@ -36,6 +37,7 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m } this.name = name + this.range = range } get type () { return 'SymbolNode' } @@ -114,7 +116,7 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * @return {SymbolNode} */ clone () { - return new SymbolNode(this.name) + return new SymbolNode(this.name, this.range) } /** @@ -163,19 +165,20 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m toJSON () { return { mathjs: 'SymbolNode', - name: this.name + name: this.name, + ...(this.range && { range: this.range }) } } /** * Instantiate a SymbolNode from its JSON representation * @param {Object} json An object structured like - * `{"mathjs": "SymbolNode", name: "x"}`, + * `{"mathjs": "SymbolNode", name: "x", range: [1, 2]}`, * where mathjs is optional * @returns {SymbolNode} */ static fromJSON (json) { - return new SymbolNode(json.name) + return new SymbolNode(json.name, json.range) } /** diff --git a/src/expression/parse.js b/src/expression/parse.js index b376025d87..d349e2ae6c 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -682,10 +682,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token === '=') { if (isSymbolNode(node)) { // parse a variable assignment like 'a = 2/3' - name = node.name getTokenSkipNewline(state) value = parseAssignment(state) - return new AssignmentNode(new SymbolNode(name), value) + return new AssignmentNode(node, value) } else if (isAccessorNode(node)) { // parse a matrix subset assignment like 'A[1,2] = 4' getTokenSkipNewline(state) @@ -1323,15 +1322,16 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.tokenType === TOKENTYPE.SYMBOL || (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) { name = state.token + const range = [state.index - name.length, state.index] getToken(state) if (hasOwnProperty(CONSTANTS, name)) { // true, false, null, ... - node = new ConstantNode(CONSTANTS[name]) + node = new ConstantNode(CONSTANTS[name], range) } else if (NUMERIC_CONSTANTS.includes(name)) { // NaN, Infinity - node = new ConstantNode(numeric(name, 'number')) + node = new ConstantNode(numeric(name, 'number'), range) } else { - node = new SymbolNode(name) + node = new SymbolNode(name, range) } // parse function parameters and matrix index @@ -1424,7 +1424,8 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ throw createSyntaxError(state, 'Property name expected after dot') } - params.push(new ConstantNode(state.token)) + const nodeRange = [state.index - state.token.length - 1, state.index] + params.push(new ConstantNode(state.token, nodeRange)) getToken(state) const dotNotation = true @@ -1446,8 +1447,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.token === '"' || state.token === "'") { str = parseStringToken(state, state.token) + const range = [state.index - str.length - 1, state.index] // create constant - node = new ConstantNode(str) + node = new ConstantNode(str, range) // parse index parameters node = parseAccessors(state, node) @@ -1662,12 +1664,13 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ if (state.tokenType === TOKENTYPE.NUMBER) { // this is a number numberStr = state.token + const range = [state.index - numberStr.length, state.index] getToken(state) const numericType = safeNumberType(numberStr, config) const value = numeric(numberStr, numericType) - return new ConstantNode(value) + return new ConstantNode(value, range) } return parseParentheses(state) diff --git a/src/function/algebra/resolve.js b/src/function/algebra/resolve.js index 6a1249ed93..669f100690 100644 --- a/src/function/algebra/resolve.js +++ b/src/function/algebra/resolve.js @@ -63,7 +63,9 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({ if (isNode(value)) { const nextWithin = new Set(within) nextWithin.add(node.name) - return _resolve(value, scope, nextWithin) + // It doesn't make sense to preserve the range property while substituting nodes. Drop it by cloning deeply + // the node. + return _resolve(value.cloneDeep(), scope, nextWithin) } else if (typeof value === 'number') { return parse(String(value)) } else if (value !== undefined) { diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index 09455c5dc0..06eb468ce9 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -1089,17 +1089,17 @@ describe('parse', function () { it('should parse constants', function () { assert.strictEqual(parse('true').type, 'ConstantNode') - assert.deepStrictEqual(parse('true'), new ConstantNode(true)) - assert.deepStrictEqual(parse('false'), new ConstantNode(false)) - assert.deepStrictEqual(parse('null'), new ConstantNode(null)) - assert.deepStrictEqual(parse('undefined'), new ConstantNode(undefined)) + assert.deepStrictEqual(parse('true'), new ConstantNode(true, [0, 4])) + assert.deepStrictEqual(parse('false'), new ConstantNode(false, [0, 5])) + assert.deepStrictEqual(parse('null'), new ConstantNode(null, [0, 4])) + assert.deepStrictEqual(parse('undefined'), new ConstantNode(undefined, [0, 9])) }) it('should parse numeric constants', function () { const nanConstantNode = parse('NaN') assert.deepStrictEqual(nanConstantNode.type, 'ConstantNode') assert.ok(isNaN(nanConstantNode.value)) - assert.deepStrictEqual(parse('Infinity'), new ConstantNode(Infinity)) + assert.deepStrictEqual(parse('Infinity'), new ConstantNode(Infinity, [0, 8])) }) it('should evaluate constants', function () { diff --git a/test/unit-tests/function/algebra/resolve.test.js b/test/unit-tests/function/algebra/resolve.test.js index c0e73b7504..9d7effa718 100644 --- a/test/unit-tests/function/algebra/resolve.test.js +++ b/test/unit-tests/function/algebra/resolve.test.js @@ -45,7 +45,7 @@ describe('resolve', function () { it('should operate directly on strings', function () { const collapsingScope = { x: math.parse('y'), y: math.parse('z') } - assert.deepStrictEqual(math.resolve('x+y', { x: 1 }), math.parse('1 + y')) + assert.deepStrictEqual(math.resolve('x + y', { x: 1 }), math.parse('1 + y')) assert.deepStrictEqual( math.resolve('x + y', collapsingScope), math.parse('z + z')) @@ -59,7 +59,7 @@ describe('resolve', function () { math.resolve(math.parse('x+y'), new Map([['x', 1]])).toString(), '1 + y' ) // direct assert.deepStrictEqual( - math.resolve('x+y', new Map([['x', 1]])), math.parse('1 + y')) + math.resolve('x + y', new Map([['x', 1]])), math.parse('1 + y')) 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')]])) diff --git a/test/unit-tests/json/replacer.test.js b/test/unit-tests/json/replacer.test.js index 1284aaa080..5501722c27 100644 --- a/test/unit-tests/json/replacer.test.js +++ b/test/unit-tests/json/replacer.test.js @@ -150,13 +150,15 @@ describe('replacer', function () { args: [ { mathjs: 'ConstantNode', - value: 2 + value: 2, + range: [0, 1] }, { mathjs: 'FunctionNode', fn: { mathjs: 'SymbolNode', - name: 'sin' + name: 'sin', + range: [4, 7] }, args: [ { @@ -166,11 +168,13 @@ describe('replacer', function () { args: [ { mathjs: 'ConstantNode', - value: 3 + value: 3, + range: [8, 9] }, { mathjs: 'SymbolNode', - name: 'x' + name: 'x', + range: [10, 11] } ], implicit: true,