Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/expression/node/ConstantNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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)
}

/**
Expand Down
13 changes: 8 additions & 5 deletions src/expression/node/SymbolNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ 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') {
throw new TypeError('String expected for parameter "name"')
}

this.name = name
this.range = range
}

get type () { return 'SymbolNode' }
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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)
}

/**
Expand Down
19 changes: 11 additions & 8 deletions src/expression/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/function/algebra/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 5 additions & 5 deletions test/unit-tests/expression/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
4 changes: 2 additions & 2 deletions test/unit-tests/function/algebra/resolve.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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')]]))
Expand Down
12 changes: 8 additions & 4 deletions test/unit-tests/json/replacer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand All @@ -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,
Expand Down