UNPKG

9.82 kBJavaScriptView Raw
1'use strict'
2
3const map = require('../../utils/array').map
4const escape = require('../../utils/string').escape
5
6function factory (type, config, load, typed) {
7 const Node = load(require('./Node'))
8 const Range = load(require('../../type/matrix/Range'))
9
10 const isArray = Array.isArray
11
12 /**
13 * @constructor IndexNode
14 * @extends Node
15 *
16 * Describes a subset of a matrix or an object property.
17 * Cannot be used on its own, needs to be used within an AccessorNode or
18 * AssignmentNode.
19 *
20 * @param {Node[]} dimensions
21 * @param {boolean} [dotNotation=false] Optional property describing whether
22 * this index was written using dot
23 * notation like `a.b`, or using bracket
24 * notation like `a["b"]` (default).
25 * Used to stringify an IndexNode.
26 */
27 function IndexNode (dimensions, dotNotation) {
28 if (!(this instanceof IndexNode)) {
29 throw new SyntaxError('Constructor must be called with the new operator')
30 }
31
32 this.dimensions = dimensions
33 this.dotNotation = dotNotation || false
34
35 // validate input
36 if (!isArray(dimensions) || !dimensions.every(type.isNode)) {
37 throw new TypeError('Array containing Nodes expected for parameter "dimensions"')
38 }
39 if (this.dotNotation && !this.isObjectProperty()) {
40 throw new Error('dotNotation only applicable for object properties')
41 }
42
43 // TODO: deprecated since v3, remove some day
44 const deprecated = function () {
45 throw new Error('Property `IndexNode.object` is deprecated, use `IndexNode.fn` instead')
46 }
47 Object.defineProperty(this, 'object', { get: deprecated, set: deprecated })
48 }
49
50 IndexNode.prototype = new Node()
51
52 IndexNode.prototype.type = 'IndexNode'
53
54 IndexNode.prototype.isIndexNode = true
55
56 /**
57 * Compile a node into a JavaScript function.
58 * This basically pre-calculates as much as possible and only leaves open
59 * calculations which depend on a dynamic scope with variables.
60 * @param {Object} math Math.js namespace with functions and constants.
61 * @param {Object} argNames An object with argument names as key and `true`
62 * as value. Used in the SymbolNode to optimize
63 * for arguments from user assigned functions
64 * (see FunctionAssignmentNode) or special symbols
65 * like `end` (see IndexNode).
66 * @return {function} Returns a function which can be called like:
67 * evalNode(scope: Object, args: Object, context: *)
68 */
69 IndexNode.prototype._compile = function (math, argNames) {
70 // TODO: implement support for bignumber (currently bignumbers are silently
71 // reduced to numbers when changing the value to zero-based)
72
73 // TODO: Optimization: when the range values are ConstantNodes,
74 // we can beforehand resolve the zero-based value
75
76 // optimization for a simple object property
77 const evalDimensions = map(this.dimensions, function (range, i) {
78 if (type.isRangeNode(range)) {
79 if (range.needsEnd()) {
80 // create a range containing end (like '4:end')
81 const childArgNames = Object.create(argNames)
82 childArgNames['end'] = true
83
84 const evalStart = range.start._compile(math, childArgNames)
85 const evalEnd = range.end._compile(math, childArgNames)
86 const evalStep = range.step
87 ? range.step._compile(math, childArgNames)
88 : function () { return 1 }
89
90 return function evalDimension (scope, args, context) {
91 const size = math.size(context).valueOf()
92 const childArgs = Object.create(args)
93 childArgs['end'] = size[i]
94
95 return createRange(
96 evalStart(scope, childArgs, context),
97 evalEnd(scope, childArgs, context),
98 evalStep(scope, childArgs, context)
99 )
100 }
101 } else {
102 // create range
103 const evalStart = range.start._compile(math, argNames)
104 const evalEnd = range.end._compile(math, argNames)
105 const evalStep = range.step
106 ? range.step._compile(math, argNames)
107 : function () { return 1 }
108
109 return function evalDimension (scope, args, context) {
110 return createRange(
111 evalStart(scope, args, context),
112 evalEnd(scope, args, context),
113 evalStep(scope, args, context)
114 )
115 }
116 }
117 } else if (type.isSymbolNode(range) && range.name === 'end') {
118 // SymbolNode 'end'
119 const childArgNames = Object.create(argNames)
120 childArgNames['end'] = true
121
122 const evalRange = range._compile(math, childArgNames)
123
124 return function evalDimension (scope, args, context) {
125 const size = math.size(context).valueOf()
126 const childArgs = Object.create(args)
127 childArgs['end'] = size[i]
128
129 return evalRange(scope, childArgs, context)
130 }
131 } else {
132 // ConstantNode
133 const evalRange = range._compile(math, argNames)
134 return function evalDimension (scope, args, context) {
135 return evalRange(scope, args, context)
136 }
137 }
138 })
139
140 return function evalIndexNode (scope, args, context) {
141 const dimensions = map(evalDimensions, function (evalDimension) {
142 return evalDimension(scope, args, context)
143 })
144 return math.index.apply(math, dimensions)
145 }
146 }
147
148 /**
149 * Execute a callback for each of the child nodes of this node
150 * @param {function(child: Node, path: string, parent: Node)} callback
151 */
152 IndexNode.prototype.forEach = function (callback) {
153 for (let i = 0; i < this.dimensions.length; i++) {
154 callback(this.dimensions[i], 'dimensions[' + i + ']', this)
155 }
156 }
157
158 /**
159 * Create a new IndexNode having it's childs be the results of calling
160 * the provided callback function for each of the childs of the original node.
161 * @param {function(child: Node, path: string, parent: Node): Node} callback
162 * @returns {IndexNode} Returns a transformed copy of the node
163 */
164 IndexNode.prototype.map = function (callback) {
165 const dimensions = []
166 for (let i = 0; i < this.dimensions.length; i++) {
167 dimensions[i] = this._ifNode(callback(this.dimensions[i], 'dimensions[' + i + ']', this))
168 }
169
170 return new IndexNode(dimensions)
171 }
172
173 /**
174 * Create a clone of this node, a shallow copy
175 * @return {IndexNode}
176 */
177 IndexNode.prototype.clone = function () {
178 return new IndexNode(this.dimensions.slice(0))
179 }
180
181 /**
182 * Test whether this IndexNode contains a single property name
183 * @return {boolean}
184 */
185 IndexNode.prototype.isObjectProperty = function () {
186 return this.dimensions.length === 1 &&
187 type.isConstantNode(this.dimensions[0]) &&
188 typeof this.dimensions[0].value === 'string'
189 }
190
191 /**
192 * Returns the property name if IndexNode contains a property.
193 * If not, returns null.
194 * @return {string | null}
195 */
196 IndexNode.prototype.getObjectProperty = function () {
197 return this.isObjectProperty() ? this.dimensions[0].value : null
198 }
199
200 /**
201 * Get string representation
202 * @param {Object} options
203 * @return {string} str
204 */
205 IndexNode.prototype._toString = function (options) {
206 // format the parameters like "[1, 0:5]"
207 return this.dotNotation
208 ? ('.' + this.getObjectProperty())
209 : ('[' + this.dimensions.join(', ') + ']')
210 }
211
212 /**
213 * Get a JSON representation of the node
214 * @returns {Object}
215 */
216 IndexNode.prototype.toJSON = function () {
217 return {
218 mathjs: 'IndexNode',
219 dimensions: this.dimensions,
220 dotNotation: this.dotNotation
221 }
222 }
223
224 /**
225 * Instantiate an IndexNode from its JSON representation
226 * @param {Object} json An object structured like
227 * `{"mathjs": "IndexNode", dimensions: [...], dotNotation: false}`,
228 * where mathjs is optional
229 * @returns {IndexNode}
230 */
231 IndexNode.fromJSON = function (json) {
232 return new IndexNode(json.dimensions, json.dotNotation)
233 }
234
235 /**
236 * Get HTML representation
237 * @param {Object} options
238 * @return {string} str
239 */
240 IndexNode.prototype.toHTML = function (options) {
241 // format the parameters like "[1, 0:5]"
242 const dimensions = []
243 for (let i = 0; i < this.dimensions.length; i++) {
244 dimensions[i] = this.dimensions[i].toHTML()
245 }
246 if (this.dotNotation) {
247 return '<span class="math-operator math-accessor-operator">.</span>' + '<span class="math-symbol math-property">' + escape(this.getObjectProperty()) + '</span>'
248 } else {
249 return '<span class="math-parenthesis math-square-parenthesis">[</span>' + dimensions.join('<span class="math-separator">,</span>') + '<span class="math-parenthesis math-square-parenthesis">]</span>'
250 }
251 }
252
253 /**
254 * Get LaTeX representation
255 * @param {Object} options
256 * @return {string} str
257 */
258 IndexNode.prototype._toTex = function (options) {
259 const dimensions = this.dimensions.map(function (range) {
260 return range.toTex(options)
261 })
262
263 return this.dotNotation
264 ? ('.' + this.getObjectProperty() + '')
265 : ('_{' + dimensions.join(',') + '}')
266 }
267
268 // helper function to create a Range from start, step and end
269 function createRange (start, end, step) {
270 return new Range(
271 type.isBigNumber(start) ? start.toNumber() : start,
272 type.isBigNumber(end) ? end.toNumber() : end,
273 type.isBigNumber(step) ? step.toNumber() : step
274 )
275 }
276
277 return IndexNode
278}
279
280exports.name = 'IndexNode'
281exports.path = 'expression.node'
282exports.factory = factory