Skip to content

Commit 3c49fca

Browse files
authored
Fix #2532: matrix index symbol end not working when used inside a sub-expression (#2535)
* Fix #2532: matrix index symbol `end` not working when used inside a sub-expression * Refactor IndexNode.prototype._compile: remove redundant logic * Throw a runtime exception when the context of an IndexNode is not a Matrix, Array, or string * Describe the behavior of variable `end` nested indices * Oopsie, fix linting issues * Add a unit test to validate that `end` resolves to the inner context in case of nested indices
1 parent bb963ca commit 3c49fca

File tree

4 files changed

+88
-64
lines changed

4 files changed

+88
-64
lines changed

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- Fix #2526, #2529: improve TypeScript definitions of function `round`, `fix`,
66
`floor`, `ceil`, and `nthRoot`, and improved the number only implementations
77
of those functions (#2531, #2539). Thanks @simlaticak and @gwhitney.
8+
- Fix #2532: matrix index symbol `end` not working when used inside
9+
a sub-expression.
810

911

1012
# 2022-04-19, version 10.5.0

docs/expressions/syntax.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,8 +549,11 @@ can be replaced by using indexes. Indexes are enclosed in square brackets, and
549549
contain a number or a range for each of the matrix dimensions. A range can have
550550
its start and/or end undefined. When the start is undefined, the range will start
551551
at 1, when the end is undefined, the range will end at the end of the matrix.
552+
552553
There is a context variable `end` available as well to denote the end of the
553-
matrix.
554+
matrix. This variable cannot be used in multiple nested indices. In that case,
555+
`end` will be resolved as the end of the innermost matrix. To solve this,
556+
resolving of the nested index needs to be split in two separate operations.
554557

555558
*IMPORTANT: matrix indexes and ranges work differently from the math.js indexes
556559
in JavaScript: They are one-based with an included upper-bound, similar to most

src/expression/node/IndexNode.js

Lines changed: 21 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { isBigNumber, isConstantNode, isNode, isRangeNode, isSymbolNode } from '../../utils/is.js'
21
import { map } from '../../utils/array.js'
3-
import { escape } from '../../utils/string.js'
4-
import { factory } from '../../utils/factory.js'
52
import { getSafeProperty } from '../../utils/customs.js'
3+
import { factory } from '../../utils/factory.js'
4+
import { isArray, isConstantNode, isMatrix, isNode, isString, typeOf } from '../../utils/is.js'
5+
import { escape } from '../../utils/string.js'
66

77
const name = 'IndexNode'
88
const dependencies = [
9-
'Range',
109
'Node',
1110
'size'
1211
]
1312

14-
export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ Range, Node, size }) => {
13+
export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ Node, size }) => {
1514
/**
1615
* @constructor IndexNode
1716
* @extends Node
@@ -71,66 +70,34 @@ export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ Ra
7170
// we can beforehand resolve the zero-based value
7271

7372
// optimization for a simple object property
74-
const evalDimensions = map(this.dimensions, function (range, i) {
75-
if (isRangeNode(range)) {
76-
if (range.needsEnd()) {
77-
// create a range containing end (like '4:end')
78-
const childArgNames = Object.create(argNames)
79-
childArgNames.end = true
80-
81-
const evalStart = range.start._compile(math, childArgNames)
82-
const evalEnd = range.end._compile(math, childArgNames)
83-
const evalStep = range.step
84-
? range.step._compile(math, childArgNames)
85-
: function () { return 1 }
86-
87-
return function evalDimension (scope, args, context) {
88-
const s = size(context).valueOf()
89-
const childArgs = Object.create(args)
90-
childArgs.end = s[i]
91-
92-
return createRange(
93-
evalStart(scope, childArgs, context),
94-
evalEnd(scope, childArgs, context),
95-
evalStep(scope, childArgs, context)
96-
)
97-
}
98-
} else {
99-
// create range
100-
const evalStart = range.start._compile(math, argNames)
101-
const evalEnd = range.end._compile(math, argNames)
102-
const evalStep = range.step
103-
? range.step._compile(math, argNames)
104-
: function () { return 1 }
105-
106-
return function evalDimension (scope, args, context) {
107-
return createRange(
108-
evalStart(scope, args, context),
109-
evalEnd(scope, args, context),
110-
evalStep(scope, args, context)
111-
)
112-
}
113-
}
114-
} else if (isSymbolNode(range) && range.name === 'end') {
115-
// SymbolNode 'end'
73+
const evalDimensions = map(this.dimensions, function (dimension, i) {
74+
const needsEnd = dimension
75+
.filter(node => node.isSymbolNode && node.name === 'end')
76+
.length > 0
77+
78+
if (needsEnd) {
79+
// SymbolNode 'end' is used inside the index,
80+
// like in `A[end]` or `A[end - 2]`
11681
const childArgNames = Object.create(argNames)
11782
childArgNames.end = true
11883

119-
const evalRange = range._compile(math, childArgNames)
84+
const _evalDimension = dimension._compile(math, childArgNames)
12085

12186
return function evalDimension (scope, args, context) {
87+
if (!isMatrix(context) && !isArray(context) && !isString(context)) {
88+
throw new TypeError('Cannot resolve "end": ' +
89+
'context must be a Matrix, Array, or string but is ' + typeOf(context))
90+
}
91+
12292
const s = size(context).valueOf()
12393
const childArgs = Object.create(args)
12494
childArgs.end = s[i]
12595

126-
return evalRange(scope, childArgs, context)
96+
return _evalDimension(scope, childArgs, context)
12797
}
12898
} else {
129-
// ConstantNode
130-
const evalRange = range._compile(math, argNames)
131-
return function evalDimension (scope, args, context) {
132-
return evalRange(scope, args, context)
133-
}
99+
// SymbolNode `end` not used
100+
return dimension._compile(math, argNames)
134101
}
135102
})
136103

@@ -265,14 +232,5 @@ export const createIndexNode = /* #__PURE__ */ factory(name, dependencies, ({ Ra
265232
: ('_{' + dimensions.join(',') + '}')
266233
}
267234

268-
// helper function to create a Range from start, step and end
269-
function createRange (start, end, step) {
270-
return new Range(
271-
isBigNumber(start) ? start.toNumber() : start,
272-
isBigNumber(end) ? end.toNumber() : end,
273-
isBigNumber(step) ? step.toNumber() : step
274-
)
275-
}
276-
277235
return IndexNode
278236
}, { isClass: true, isNode: true })

test/unit-tests/expression/node/AccessorNode.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ describe('AccessorNode', function () {
7676
assert.deepStrictEqual(expr.evaluate(scope), [[3, 4]])
7777
})
7878

79+
it('should compile a AccessorNode with "end" in an expression', function () {
80+
const a = new SymbolNode('a')
81+
const index = new IndexNode([
82+
new OperatorNode(
83+
'-',
84+
'subtract',
85+
[
86+
new SymbolNode('end'),
87+
new ConstantNode(2)
88+
]
89+
)
90+
])
91+
const n = new AccessorNode(a, index)
92+
const expr = n.compile()
93+
94+
const scope = {
95+
a: [1, 2, 3, 4]
96+
}
97+
assert.deepStrictEqual(expr.evaluate(scope), 2)
98+
})
99+
79100
it('should compile a AccessorNode with a property', function () {
80101
const a = new SymbolNode('a')
81102
const index = new IndexNode([new ConstantNode('b')])
@@ -185,6 +206,46 @@ describe('AccessorNode', function () {
185206
assert.deepStrictEqual(expr.evaluate(scope), [[3, 4]])
186207
})
187208

209+
it('should use the inner context when using "end" in a nested index', function () {
210+
// A[B[end]]
211+
const node = new AccessorNode(
212+
new SymbolNode('A'),
213+
new IndexNode([
214+
new AccessorNode(
215+
new SymbolNode('B'),
216+
new IndexNode([
217+
new SymbolNode('end')
218+
])
219+
)
220+
])
221+
)
222+
223+
// here, end should resolve to the end of B, which is 3 (whilst the end of A is 6)
224+
const expr = node.compile()
225+
const scope = {
226+
A: [4, 5, 6, 7, 8, 9],
227+
B: [1, 2, 3]
228+
}
229+
assert.deepStrictEqual(expr.evaluate(scope), 6)
230+
})
231+
232+
it('should give a proper error message when using "end" inside the index of an object', function () {
233+
const obj = new SymbolNode('value')
234+
const index = new IndexNode([
235+
new SymbolNode('end')
236+
])
237+
const n = new AccessorNode(obj, index)
238+
const expr = n.compile()
239+
240+
assert.throws(function () {
241+
expr.evaluate({ value: { end: true } })
242+
}, /TypeError: Cannot resolve "end": context must be a Matrix, Array, or string but is Object/)
243+
244+
assert.throws(function () {
245+
expr.evaluate({ value: 42 })
246+
}, /TypeError: Cannot resolve "end": context must be a Matrix, Array, or string but is number/)
247+
})
248+
188249
it('should compile a AccessorNode with bignumber setting', function () {
189250
const a = new bigmath.SymbolNode('a')
190251
const b = new bigmath.ConstantNode(2)

0 commit comments

Comments
 (0)