This repository was archived by the owner on Aug 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 279
/
Copy pathflattenOperands.js
350 lines (328 loc) · 13 KB
/
flattenOperands.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
const evaluate = require('./evaluate');
const Negative = require('../Negative');
const Node = require('../node');
/*
Background:
Expression trees are commonly parsed as binary trees, and mathjs does this too.
That means that a mathjs expression tree likely looks like:
http://collegelabs.co/clabs/nld/images/524px-Expression_Tree.svg.png
e.g. 2+2+2 is parsed by mathjs as 2 + 2+2 (a plus node with children 2 and 2+2)
However...
1. This is more complicated than needed. 2+2+2 is the same as 2+(2+2)
2. To collect like terms, we actually *need* it to be flat. e.g. with 2x+(2+2x),
there's no easy way to know that there are two 2x's to collect without
running up and down the tree. If we flatten to 2x+2+2x, it becomes a lot
easier to collect like terms to (2x+2x) + 2, which would then be combined to
4x + 2
The purpose of flatteOperands is to flatten the tree in this way.
e.g. an expression that is grouped in the tree like
(2 + ((4 * ((1 + 2) + (3 + 4))) * 8))
should be flattened to look like:
(2 + (4 * (1 + 2 + 3 + 4) * 8))
Subtraction and division are also flattened, though that gets a bit more
complicated and you may as well start reading through the code if you're
interested in how that works
*/
// Flattens the tree accross the same operation (just + and * for now)
// e.g. 2+2+2 is parsed by mathjs as 2+(2+2), but this would change that to
// 2+2+2, ie one + node that has three children.
// Input: an expression tree
// Output: the expression tree updated with flattened operations
function flattenOperands(node) {
// If the node is a mixed number, do not perform any flattening
// -- Flattening will take out the implicit multiplication, and so
// it will be impossible to tell if the node is a mixed number or
// if it is legitimate multiplication
// -- Converting fractions happens before any other simplification step,
// so the tree *will* get flattened before any other changes happen
if (Node.MixedNumber.isMixedNumber(node)) {
return node;
}
if (Node.Type.isConstant(node, true)) {
// the evaluate() changes unary minuses around constant nodes to constant nodes
// with negative values.
const constNode = Node.Creator.constant(evaluate(node));
if (node.changeGroup) {
constNode.changeGroup = node.changeGroup;
}
return constNode;
}
else if (Node.Type.isOperator(node)) {
if ('+-/*'.includes(node.op)) {
let parentOp;
if (node.op === '/') {
// Division is flattened in partner with multiplication. This means
// that after collecting the operands, they'll be children args of *
parentOp = '*';
}
else if (node.op === '-') {
// Subtraction is flattened in partner with addition, This means that
// after collecting the operands, they'll be children args of +
parentOp = '+';
}
else {
parentOp = node.op;
}
return flattenSupportedOperation(node, parentOp);
}
// If the operation is not supported, just recurse on the children
else {
node.args.forEach((child, i) => {
node.args[i] = flattenOperands(child);
});
}
return node;
}
else if (Node.Type.isParenthesis(node)) {
node.content = flattenOperands(node.content);
return node;
}
else if (Node.Type.isUnaryMinus(node)) {
const arg = flattenOperands(node.args[0]);
const flattenedNode = Negative.negate(arg, true);
if (node.changeGroup) {
flattenedNode.changeGroup = node.changeGroup;
}
return flattenedNode;
}
else if (Node.Type.isFunction(node) && node.fn.args) {
// node.fn.args will only be populated in the case where a function node is
// followed by a node in parenthesis
// mathjs parses this incorrectly; we want to convert the node to be
// multiplication between the function and the node in parenthesis
// e.g. nthRoot(11)(x+y) -> nthRoot(11) * (x+y)
// abs(3)(1+2) -> abs(3) * (1+2)
const flattenedFn = flattenOperands(node.fn); // e.g. nthRoot(11)
const flattenedArg = flattenOperands(node.args[0]); // e.g. x+y
const newNode = Node.Creator.operator(
'*', [flattenedFn, Node.Creator.parenthesis(flattenedArg)]);
return newNode;
}
else if (Node.Type.isFunction(node, 'abs')) {
node.args[0] = flattenOperands(node.args[0]);
return node;
}
else if (Node.Type.isFunction(node, 'nthRoot')) {
node.args[0] = flattenOperands(node.args[0]);
if (node.args[1]) {
node.args[1] = flattenOperands(node.args[1]);
}
return node;
}
else {
return node;
}
}
// Flattens operations (see flattenOperands docstring) for an operator node
// with an operation type that can be flattened. Currently * + / are supported.
// Returns the updated, flattened node.
// NOTE: the returned node will be of operation type `parentOp`, regardless of
// the operation type of `node`, unless `node` wasn't changed
// e.g. 2 * 3 / 4 would be * of 2 and 3/4, but 2/3 would stay 2/3 and division
function flattenSupportedOperation(node, parentOp) {
// First get the list of operands that this operator operates on.
// e.g. 2 + 3 + 4 + 5 is stored as (((2 + 3) + 4) + 5) in the tree and we
// want to get the list [2, 3, 4, 5]
const operands = getOperands(node, parentOp);
// If there's only one operand (possible if 2*x was flattened to 2x)
// then it's no longer an operation, so we should replace the node
// with the one operand.
if (operands.length === 1) {
node = operands[0];
}
else {
// When we are dealing with flattening division, and there's also
// multiplication involved, we might end up with a top level * instead.
// e.g. 2*4/5 is parsed with / at the top, but in the end we want 2 * (4/5)
// Check for this by first checking if we have more than two operands
// (which is impossible for division), then by recursing through the
// original tree for any multiplication node - if there was one, it would
// have ended up at the root.
if (node.op === '/' && (operands.length > 2 ||
hasMultiplicationBesideDivision(node))) {
node = Node.Creator.operator('*', operands);
}
// similarily, - will become + always
else if (node.op === '-') {
node = Node.Creator.operator('+', operands);
}
// otherwise keep the operator, replace operands
else {
node.args = operands;
}
// When we collect operands to flatten multiplication, the
// multiplication of those operands should never be implicit
if (node.op === '*') {
node.implicit = false;
}
}
return node;
}
// Recursively finds the operands under `parentOp` in the input tree `node`.
// The input tree `node` will always have a parent that is an operation
// of type `op`.
// Op is a string e.g. '+' or '*'
// returns the list of all the node operated on by `parentOp`
function getOperands(node, parentOp) {
// We can only recurse on operations of type op.
// If the node is not an operator node or of the right operation type,
// we can't break up or flatten this tree any further, so we return just
// the current node, and recurse on it to flatten its ops.
if (!Node.Type.isOperator(node)) {
return [flattenOperands(node)];
}
switch (node.op) {
// division is part of flattening multiplication
case '*':
case '/':
if (parentOp !== '*') {
return [flattenOperands(node)];
}
break;
case '+':
case '-':
if (parentOp !== '+') {
return [flattenOperands(node)];
}
break;
default:
return [flattenOperands(node)];
}
if (Node.PolynomialTerm.isPolynomialTerm(node, true)) {
node.args.forEach((arg, i) => {
node.args[i] = flattenOperands(node.args[i]);
});
return [node];
}
// If we're flattening over *, check for a polynomial term (ie a
// coefficient multiplied by a symbol such as 2x^2 or 3y)
// This is true if there's an implicit multiplication and the right operand
// is a symbol or a symbol to an exponent.
else if (parentOp === '*' && isPolynomialTermMultiplication(node)) {
return maybeFlattenPolynomialTerm(node);
}
else if (parentOp === '*' && node.op === '/') {
return flattenDivision(node);
}
else if (node.op === '-') {
// this operation will become addition e.g. 2 - 3 -> 2 + -(-3)
const secondOperand = node.args[1];
const negativeSecondOperand = Negative.negate(secondOperand, true);
const operands = [
getOperands(node.args[0], parentOp),
getOperands(negativeSecondOperand, parentOp)
];
return [].concat.apply([], operands);
}
else {
const operands = [];
node.args.forEach((child) => {
// This will make an array of arrays
operands.push(getOperands(child, parentOp));
});
return [].concat.apply([], operands);
}
}
// Return true iff node is a candidate for simplifying to a polynomial
// term. This function is a helper function for getOperands.
// Context: Usually we'd flatten 2*2*x to a multiplication node with 3 children
// (2, 2, and x) but if we got 2*2x, we want to keep 2x together.
// 2*2*x (a tree stored in two levels because initially nodes only have two
// children) in the flattening process should be turned into 2*2x instead of
// 2*2*x (which has three children).
// So this function would return true for the input 2*2x, if it was stored as
// an expression tree with root node * and children 2*2 and x
function isPolynomialTermMultiplication(node) {
// This concept only applies when we're flattening multiplication operations
if (node.op !== '*') {
return false;
}
// This only makes sense when we're flattening two arguments
if (node.args.length !== 2) {
return false;
}
// The second node should be for the form x or x^2 (ie a polynomial term
// with no coefficient)
const secondOperand = node.args[1];
if (Node.PolynomialTerm.isPolynomialTerm(secondOperand)) {
const polyNode = new Node.PolynomialTerm(secondOperand);
return !polyNode.hasCoeff();
}
else {
return false;
}
}
// Takes a node that might represent a multiplication with a polynomial term
// and flattens it appropriately so the coefficient and symbol are grouped
// together. Returns a new list of operands from this node that should be
// multiplied together.
function maybeFlattenPolynomialTerm(node) {
// We recurse on the left side of the tree to find operands so far
const operands = getOperands(node.args[0], '*');
// If the last operand (so far) under * was a constant, then it's a
// polynomial term.
// e.g. 2*5*6x creates a tree where the top node is implicit multiplcation
// and the left branch goes to the tree with 2*5*6, and the right operand
// is the symbol x. We want to check that the last argument on the left (in
// this example 6) is a constant.
const lastOperand = operands.pop();
// in the above example, node.args[1] would be the symbol x
const nextOperand = flattenOperands(node.args[1]);
// a coefficient can be constant or a fraction of constants
if (Node.Type.isConstantOrConstantFraction(lastOperand)) {
// we replace the constant (which we popped) with constant*symbol
operands.push(
Node.Creator.operator('*', [lastOperand, nextOperand], true));
}
// Now we know it isn't a polynomial term, it's just another seperate operand
else {
operands.push(lastOperand);
operands.push(nextOperand);
}
return operands;
}
// Takes a division node and returns a list of operands
// If there is multiplication in the numerator, the operands returned
// are to be multiplied together. Otherwise, a list of length one with
// just the division node is returned. getOperands might change the
// operator accordingly.
function flattenDivision(node) {
// We recurse on the left side of the tree to find operands so far
// Flattening division is always considered part of a bigger picture
// of multiplication, so we get operands with '*'
let operands = getOperands(node.args[0], '*');
if (operands.length === 1) {
node.args[0] = operands.pop();
node.args[1] = flattenOperands(node.args[1]);
operands = [node];
}
else {
// This is the last operand, the term we'll want to add our division to
const numerator = operands.pop();
// This is the denominator of the current division node we're recursing on
const denominator = flattenOperands(node.args[1]);
// Note that this means 2 * 3 * 4 / 5 / 6 * 7 will flatten but keep the 4/5/6
// as an operand - in simplifyDivision.js this is changed to 4/(5*6)
const divisionNode = Node.Creator.operator('/', [numerator, denominator]);
operands.push(divisionNode);
}
return operands;
}
// Returns true if there is a * node nested in some division, with no other
// operators or parentheses between them.
// e.g. returns true: 2*3/4, 2 / 5 / 6 * 7 / 8
// e.g. returns false: 3/4/5, ((3*2) - 5) / 7, (2*5)/6
function hasMultiplicationBesideDivision(node) {
if (!Node.Type.isOperator(node)) {
return false;
}
if (node.op === '*') {
return true;
}
// we ony recurse through division
if (node.op !== '/') {
return false;
}
return node.args.some(hasMultiplicationBesideDivision);
}
module.exports = flattenOperands;