UNPKG

9.55 kBJavaScriptView Raw
1import { isBigNumber, isComplex, isNode, isUnit, typeOf } from '../../utils/is'
2import { factory } from '../../utils/factory'
3import { getPrecedence } from '../operators'
4
5const name = 'ConditionalNode'
6const dependencies = [
7 'Node'
8]
9
10export const createConditionalNode = /* #__PURE__ */ factory(name, dependencies, ({ Node }) => {
11 /**
12 * A lazy evaluating conditional operator: 'condition ? trueExpr : falseExpr'
13 *
14 * @param {Node} condition Condition, must result in a boolean
15 * @param {Node} trueExpr Expression evaluated when condition is true
16 * @param {Node} falseExpr Expression evaluated when condition is true
17 *
18 * @constructor ConditionalNode
19 * @extends {Node}
20 */
21 function ConditionalNode (condition, trueExpr, falseExpr) {
22 if (!(this instanceof ConditionalNode)) {
23 throw new SyntaxError('Constructor must be called with the new operator')
24 }
25 if (!isNode(condition)) throw new TypeError('Parameter condition must be a Node')
26 if (!isNode(trueExpr)) throw new TypeError('Parameter trueExpr must be a Node')
27 if (!isNode(falseExpr)) throw new TypeError('Parameter falseExpr must be a Node')
28
29 this.condition = condition
30 this.trueExpr = trueExpr
31 this.falseExpr = falseExpr
32 }
33
34 ConditionalNode.prototype = new Node()
35
36 ConditionalNode.prototype.type = 'ConditionalNode'
37
38 ConditionalNode.prototype.isConditionalNode = true
39
40 /**
41 * Compile a node into a JavaScript function.
42 * This basically pre-calculates as much as possible and only leaves open
43 * calculations which depend on a dynamic scope with variables.
44 * @param {Object} math Math.js namespace with functions and constants.
45 * @param {Object} argNames An object with argument names as key and `true`
46 * as value. Used in the SymbolNode to optimize
47 * for arguments from user assigned functions
48 * (see FunctionAssignmentNode) or special symbols
49 * like `end` (see IndexNode).
50 * @return {function} Returns a function which can be called like:
51 * evalNode(scope: Object, args: Object, context: *)
52 */
53 ConditionalNode.prototype._compile = function (math, argNames) {
54 const evalCondition = this.condition._compile(math, argNames)
55 const evalTrueExpr = this.trueExpr._compile(math, argNames)
56 const evalFalseExpr = this.falseExpr._compile(math, argNames)
57
58 return function evalConditionalNode (scope, args, context) {
59 return testCondition(evalCondition(scope, args, context))
60 ? evalTrueExpr(scope, args, context)
61 : evalFalseExpr(scope, args, context)
62 }
63 }
64
65 /**
66 * Execute a callback for each of the child nodes of this node
67 * @param {function(child: Node, path: string, parent: Node)} callback
68 */
69 ConditionalNode.prototype.forEach = function (callback) {
70 callback(this.condition, 'condition', this)
71 callback(this.trueExpr, 'trueExpr', this)
72 callback(this.falseExpr, 'falseExpr', this)
73 }
74
75 /**
76 * Create a new ConditionalNode having it's childs be the results of calling
77 * the provided callback function for each of the childs of the original node.
78 * @param {function(child: Node, path: string, parent: Node): Node} callback
79 * @returns {ConditionalNode} Returns a transformed copy of the node
80 */
81 ConditionalNode.prototype.map = function (callback) {
82 return new ConditionalNode(
83 this._ifNode(callback(this.condition, 'condition', this)),
84 this._ifNode(callback(this.trueExpr, 'trueExpr', this)),
85 this._ifNode(callback(this.falseExpr, 'falseExpr', this))
86 )
87 }
88
89 /**
90 * Create a clone of this node, a shallow copy
91 * @return {ConditionalNode}
92 */
93 ConditionalNode.prototype.clone = function () {
94 return new ConditionalNode(this.condition, this.trueExpr, this.falseExpr)
95 }
96
97 /**
98 * Get string representation
99 * @param {Object} options
100 * @return {string} str
101 */
102 ConditionalNode.prototype._toString = function (options) {
103 const parenthesis = (options && options.parenthesis) ? options.parenthesis : 'keep'
104 const precedence = getPrecedence(this, parenthesis)
105
106 // Enclose Arguments in parentheses if they are an OperatorNode
107 // or have lower or equal precedence
108 // NOTE: enclosing all OperatorNodes in parentheses is a decision
109 // purely based on aesthetics and readability
110 let condition = this.condition.toString(options)
111 const conditionPrecedence = getPrecedence(this.condition, parenthesis)
112 if ((parenthesis === 'all') ||
113 (this.condition.type === 'OperatorNode') ||
114 ((conditionPrecedence !== null) && (conditionPrecedence <= precedence))) {
115 condition = '(' + condition + ')'
116 }
117
118 let trueExpr = this.trueExpr.toString(options)
119 const truePrecedence = getPrecedence(this.trueExpr, parenthesis)
120 if ((parenthesis === 'all') ||
121 (this.trueExpr.type === 'OperatorNode') ||
122 ((truePrecedence !== null) && (truePrecedence <= precedence))) {
123 trueExpr = '(' + trueExpr + ')'
124 }
125
126 let falseExpr = this.falseExpr.toString(options)
127 const falsePrecedence = getPrecedence(this.falseExpr, parenthesis)
128 if ((parenthesis === 'all') ||
129 (this.falseExpr.type === 'OperatorNode') ||
130 ((falsePrecedence !== null) && (falsePrecedence <= precedence))) {
131 falseExpr = '(' + falseExpr + ')'
132 }
133 return condition + ' ? ' + trueExpr + ' : ' + falseExpr
134 }
135
136 /**
137 * Get a JSON representation of the node
138 * @returns {Object}
139 */
140 ConditionalNode.prototype.toJSON = function () {
141 return {
142 mathjs: 'ConditionalNode',
143 condition: this.condition,
144 trueExpr: this.trueExpr,
145 falseExpr: this.falseExpr
146 }
147 }
148
149 /**
150 * Instantiate an ConditionalNode from its JSON representation
151 * @param {Object} json An object structured like
152 * `{"mathjs": "ConditionalNode", "condition": ..., "trueExpr": ..., "falseExpr": ...}`,
153 * where mathjs is optional
154 * @returns {ConditionalNode}
155 */
156 ConditionalNode.fromJSON = function (json) {
157 return new ConditionalNode(json.condition, json.trueExpr, json.falseExpr)
158 }
159
160 /**
161 * Get HTML representation
162 * @param {Object} options
163 * @return {string} str
164 */
165 ConditionalNode.prototype.toHTML = function (options) {
166 const parenthesis = (options && options.parenthesis) ? options.parenthesis : 'keep'
167 const precedence = getPrecedence(this, parenthesis)
168
169 // Enclose Arguments in parentheses if they are an OperatorNode
170 // or have lower or equal precedence
171 // NOTE: enclosing all OperatorNodes in parentheses is a decision
172 // purely based on aesthetics and readability
173 let condition = this.condition.toHTML(options)
174 const conditionPrecedence = getPrecedence(this.condition, parenthesis)
175 if ((parenthesis === 'all') ||
176 (this.condition.type === 'OperatorNode') ||
177 ((conditionPrecedence !== null) && (conditionPrecedence <= precedence))) {
178 condition = '<span class="math-parenthesis math-round-parenthesis">(</span>' + condition + '<span class="math-parenthesis math-round-parenthesis">)</span>'
179 }
180
181 let trueExpr = this.trueExpr.toHTML(options)
182 const truePrecedence = getPrecedence(this.trueExpr, parenthesis)
183 if ((parenthesis === 'all') ||
184 (this.trueExpr.type === 'OperatorNode') ||
185 ((truePrecedence !== null) && (truePrecedence <= precedence))) {
186 trueExpr = '<span class="math-parenthesis math-round-parenthesis">(</span>' + trueExpr + '<span class="math-parenthesis math-round-parenthesis">)</span>'
187 }
188
189 let falseExpr = this.falseExpr.toHTML(options)
190 const falsePrecedence = getPrecedence(this.falseExpr, parenthesis)
191 if ((parenthesis === 'all') ||
192 (this.falseExpr.type === 'OperatorNode') ||
193 ((falsePrecedence !== null) && (falsePrecedence <= precedence))) {
194 falseExpr = '<span class="math-parenthesis math-round-parenthesis">(</span>' + falseExpr + '<span class="math-parenthesis math-round-parenthesis">)</span>'
195 }
196 return condition + '<span class="math-operator math-conditional-operator">?</span>' + trueExpr + '<span class="math-operator math-conditional-operator">:</span>' + falseExpr
197 }
198
199 /**
200 * Get LaTeX representation
201 * @param {Object} options
202 * @return {string} str
203 */
204 ConditionalNode.prototype._toTex = function (options) {
205 return '\\begin{cases} {' +
206 this.trueExpr.toTex(options) + '}, &\\quad{\\text{if }\\;' +
207 this.condition.toTex(options) +
208 '}\\\\{' + this.falseExpr.toTex(options) +
209 '}, &\\quad{\\text{otherwise}}\\end{cases}'
210 }
211
212 /**
213 * Test whether a condition is met
214 * @param {*} condition
215 * @returns {boolean} true if condition is true or non-zero, else false
216 */
217 function testCondition (condition) {
218 if (typeof condition === 'number' ||
219 typeof condition === 'boolean' ||
220 typeof condition === 'string') {
221 return !!condition
222 }
223
224 if (condition) {
225 if (isBigNumber(condition)) {
226 return !condition.isZero()
227 }
228
229 if (isComplex(condition)) {
230 return !!((condition.re || condition.im))
231 }
232
233 if (isUnit(condition)) {
234 return !!condition.value
235 }
236 }
237
238 if (condition === null || condition === undefined) {
239 return false
240 }
241
242 throw new TypeError('Unsupported type of condition "' + typeOf(condition) + '"')
243 }
244
245 return ConditionalNode
246}, { isClass: true, isNode: true })