UNPKG

8.45 kBJavaScriptView Raw
1// TODO this could be improved by simplifying seperated constants under associative and commutative operators
2import { isFraction, isNode, isOperatorNode } from '../../../utils/is'
3import { factory } from '../../../utils/factory'
4import { createUtil } from './util'
5import { noBignumber, noFraction } from '../../../utils/noop'
6
7const name = 'simplifyConstant'
8const dependencies = [
9 'typed',
10 'config',
11 'mathWithTransform',
12 '?fraction',
13 '?bignumber',
14 'ConstantNode',
15 'OperatorNode',
16 'FunctionNode',
17 'SymbolNode'
18]
19
20export const createSimplifyConstant = /* #__PURE__ */ factory(name, dependencies, ({
21 typed,
22 config,
23 mathWithTransform,
24 fraction,
25 bignumber,
26 ConstantNode,
27 OperatorNode,
28 FunctionNode,
29 SymbolNode
30}) => {
31 const { isCommutative, isAssociative, allChildren, createMakeNodeFunction } =
32 createUtil({ FunctionNode, OperatorNode, SymbolNode })
33
34 function simplifyConstant (expr, options) {
35 const res = foldFraction(expr, options)
36 return isNode(res) ? res : _toNode(res)
37 }
38
39 function _eval (fnname, args, options) {
40 try {
41 return _toNumber(mathWithTransform[fnname].apply(null, args), options)
42 } catch (ignore) {
43 // sometimes the implicit type conversion causes the evaluation to fail, so we'll try again after removing Fractions
44 args = args.map(function (x) {
45 if (isFraction(x)) {
46 return x.valueOf()
47 }
48 return x
49 })
50 return _toNumber(mathWithTransform[fnname].apply(null, args), options)
51 }
52 }
53
54 const _toNode = typed({
55 'Fraction': _fractionToNode,
56 'number': function (n) {
57 if (n < 0) {
58 return unaryMinusNode(new ConstantNode(-n))
59 }
60 return new ConstantNode(n)
61 },
62 'BigNumber': function (n) {
63 if (n < 0) {
64 return unaryMinusNode(new ConstantNode(-n))
65 }
66 return new ConstantNode(n) // old parameters: (n.toString(), 'number')
67 },
68 'Complex': function (s) {
69 throw new Error('Cannot convert Complex number to Node')
70 }
71 })
72
73 // convert a number to a fraction only if it can be expressed exactly
74 function _exactFraction (n, options) {
75 const exactFractions = (options && options.exactFractions !== false)
76 if (exactFractions && isFinite(n) && fraction) {
77 const f = fraction(n)
78 if (f.valueOf() === n) {
79 return f
80 }
81 }
82 return n
83 }
84
85 // Convert numbers to a preferred number type in preference order: Fraction, number, Complex
86 // BigNumbers are left alone
87 const _toNumber = typed({
88 'string, Object': function (s, options) {
89 if (config.number === 'BigNumber') {
90 if (bignumber === undefined) {
91 noBignumber()
92 }
93 return bignumber(s)
94 } else if (config.number === 'Fraction') {
95 if (fraction === undefined) {
96 noFraction()
97 }
98 return fraction(s)
99 } else {
100 const n = parseFloat(s)
101 return _exactFraction(n, options)
102 }
103 },
104
105 'Fraction, Object': function (s, options) { return s }, // we don't need options here
106
107 'BigNumber, Object': function (s, options) { return s }, // we don't need options here
108
109 'number, Object': function (s, options) {
110 return _exactFraction(s, options)
111 },
112
113 'Complex, Object': function (s, options) {
114 if (s.im !== 0) {
115 return s
116 }
117 return _exactFraction(s.re, options)
118 }
119 })
120
121 function unaryMinusNode (n) {
122 return new OperatorNode('-', 'unaryMinus', [n])
123 }
124
125 function _fractionToNode (f) {
126 let n
127 const vn = f.s * f.n
128 if (vn < 0) {
129 n = new OperatorNode('-', 'unaryMinus', [new ConstantNode(-vn)])
130 } else {
131 n = new ConstantNode(vn)
132 }
133
134 if (f.d === 1) {
135 return n
136 }
137 return new OperatorNode('/', 'divide', [n, new ConstantNode(f.d)])
138 }
139
140 /*
141 * Create a binary tree from a list of Fractions and Nodes.
142 * Tries to fold Fractions by evaluating them until the first Node in the list is hit, so
143 * `args` should be sorted to have the Fractions at the start (if the operator is commutative).
144 * @param args - list of Fractions and Nodes
145 * @param fn - evaluator for the binary operation evaluator that accepts two Fractions
146 * @param makeNode - creates a binary OperatorNode/FunctionNode from a list of child Nodes
147 * if args.length is 1, returns args[0]
148 * @return - Either a Node representing a binary expression or Fraction
149 */
150 function foldOp (fn, args, makeNode, options) {
151 return args.reduce(function (a, b) {
152 if (!isNode(a) && !isNode(b)) {
153 try {
154 return _eval(fn, [a, b], options)
155 } catch (ignoreandcontinue) {}
156 a = _toNode(a)
157 b = _toNode(b)
158 } else if (!isNode(a)) {
159 a = _toNode(a)
160 } else if (!isNode(b)) {
161 b = _toNode(b)
162 }
163
164 return makeNode([a, b])
165 })
166 }
167
168 // destroys the original node and returns a folded one
169 function foldFraction (node, options) {
170 switch (node.type) {
171 case 'SymbolNode':
172 return node
173 case 'ConstantNode':
174 if (typeof node.value === 'number' || !isNaN(node.value)) {
175 return _toNumber(node.value, options)
176 }
177 return node
178 case 'FunctionNode':
179 if (mathWithTransform[node.name] && mathWithTransform[node.name].rawArgs) {
180 return node
181 }
182
183 // Process operators as OperatorNode
184 const operatorFunctions = [ 'add', 'multiply' ]
185 if (operatorFunctions.indexOf(node.name) === -1) {
186 let args = node.args.map(arg => foldFraction(arg, options))
187
188 // If all args are numbers
189 if (!args.some(isNode)) {
190 try {
191 return _eval(node.name, args, options)
192 } catch (ignoreandcontine) {}
193 }
194
195 // Convert all args to nodes and construct a symbolic function call
196 args = args.map(function (arg) {
197 return isNode(arg) ? arg : _toNode(arg)
198 })
199 return new FunctionNode(node.name, args)
200 } else {
201 // treat as operator
202 }
203 /* falls through */
204 case 'OperatorNode':
205 const fn = node.fn.toString()
206 let args
207 let res
208 const makeNode = createMakeNodeFunction(node)
209 if (isOperatorNode(node) && node.isUnary()) {
210 args = [foldFraction(node.args[0], options)]
211 if (!isNode(args[0])) {
212 res = _eval(fn, args, options)
213 } else {
214 res = makeNode(args)
215 }
216 } else if (isAssociative(node)) {
217 args = allChildren(node)
218 args = args.map(arg => foldFraction(arg, options))
219
220 if (isCommutative(fn)) {
221 // commutative binary operator
222 const consts = []
223 const vars = []
224
225 for (let i = 0; i < args.length; i++) {
226 if (!isNode(args[i])) {
227 consts.push(args[i])
228 } else {
229 vars.push(args[i])
230 }
231 }
232
233 if (consts.length > 1) {
234 res = foldOp(fn, consts, makeNode, options)
235 vars.unshift(res)
236 res = foldOp(fn, vars, makeNode, options)
237 } else {
238 // we won't change the children order since it's not neccessary
239 res = foldOp(fn, args, makeNode, options)
240 }
241 } else {
242 // non-commutative binary operator
243 res = foldOp(fn, args, makeNode, options)
244 }
245 } else {
246 // non-associative binary operator
247 args = node.args.map(arg => foldFraction(arg, options))
248 res = foldOp(fn, args, makeNode, options)
249 }
250 return res
251 case 'ParenthesisNode':
252 // remove the uneccessary parenthesis
253 return foldFraction(node.content, options)
254 case 'AccessorNode':
255 /* falls through */
256 case 'ArrayNode':
257 /* falls through */
258 case 'AssignmentNode':
259 /* falls through */
260 case 'BlockNode':
261 /* falls through */
262 case 'FunctionAssignmentNode':
263 /* falls through */
264 case 'IndexNode':
265 /* falls through */
266 case 'ObjectNode':
267 /* falls through */
268 case 'RangeNode':
269 /* falls through */
270 case 'ConditionalNode':
271 /* falls through */
272 default:
273 throw new Error(`Unimplemented node type in simplifyConstant: ${node.type}`)
274 }
275 }
276
277 return simplifyConstant
278})