1 /* 2 Copyright 2008-2023 3 Matthias Ehmann, 4 Carsten Miller, 5 Alfred Wassermann 6 7 This file is part of JSXGraph. 8 9 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 10 11 You can redistribute it and/or modify it under the terms of the 12 13 * GNU Lesser General Public License as published by 14 the Free Software Foundation, either version 3 of the License, or 15 (at your option) any later version 16 OR 17 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 18 19 JSXGraph is distributed in the hope that it will be useful, 20 but WITHOUT ANY WARRANTY; without even the implied warranty of 21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 GNU Lesser General Public License for more details. 23 24 You should have received a copy of the GNU Lesser General Public License and 25 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 26 and <https://opensource.org/licenses/MIT/>. 27 */ 28 29 /** 30 * @fileoverview Simple prefix parser for measurements and expressions of measurements. 31 * An expression is given as 32 * <ul> 33 * <li> array starting with an operator as first element, followed 34 * by one or more operands, 35 * <li> number. 36 * </ul> 37 * <p> 38 * Possible operands are: 39 * <ul> 40 * <li> '+', '-', '*', '/' 41 * </ul> 42 * 43 * @example 44 * 45 */ 46 import JXG from "../jxg"; 47 import Type from "../utils/type"; 48 import Mat from "../math/math"; 49 import Const from "../base/constants"; 50 51 /** 52 * Prefix expression parser, i.e. a poor man's parser. 53 * This is a simple prefix parser for measurements and expressions of measurements, 54 * see {@link Measurement}. 55 * An expression is given as 56 * <ul> 57 * <li> array starting with an operator as first element, followed 58 * by one or more operands, 59 * <li> number. 60 * </ul> 61 * <p> 62 * Possible operators are: 63 * <ul> 64 * <li> '+', '-', '*', '/': binary operators 65 * <li> 'Area', 'Radius', 'Value', 'V', 'L': arbitrary methods of JSXGraph elements, supplied as strings. 66 * <li> 'exec': call a function 67 * </ul> 68 * <p> 69 * Possible operands are: 70 * <ul> 71 * <li> numbers 72 * <li> strings 73 * <li> JSXGraph elements in case the operator is a method. Example: ['Area', circle] calls 74 * the method circle.Area(). 75 * <li> prefix expressions (for binary operators) 76 * <li> 'exec': call functions. Example: ['exec', 'sin', ['V', slider]] computes 'Math.sin(slider.Value())'. 77 * As functions only functions in Math or JXG.Math are allowed. 78 * </ul> 79 * @namespace 80 * 81 * @example 82 * ['+', 100, 200] 83 * @example 84 * var p1 = board.create('point', [1, 1]); 85 * var p2 = board.create('point', [1, 3]); 86 * var seg = board.create('segment', [[-2,-3], [-2, 3]]); 87 * 88 * // Valid prefix expression: ['L', seg] 89 * 90 * @example 91 * var p1 = board.create('point', [1, 1]); 92 * var p2 = board.create('point', [1, 3]); 93 * var seg = board.create('segment', [[-2,-3], [-2, 3]]); 94 * var ci = board.create('circle', [p1, 7]); 95 * 96 * // Valid prefix expression: ['+', ['Radius', ci], ['L', seg]] 97 * 98 * @example 99 * var ang = board.create('angle', [[4, 0], [0, 0], [2, 2]]); 100 * // Valid prefix expression: ['V', ang, 'degrees']); 101 */ 102 JXG.PrefixParser = { 103 /** 104 * Parse a prefix expression and apply an action. 105 * @param {array|number} term Expression 106 * @param {String} action Determines what to do. So far, the only 107 * action available is 'execute', which evaluates the expression. 108 * @returns {Number} What ever the action does. 109 */ 110 parse: function (term, action) { 111 var method, i, le, res, fun, v; 112 113 if (Type.isNumber(term) || Type.isString(term)) { 114 return term; 115 } 116 if (!Type.isArray(term) || term.length < 2) { 117 throw new Error('prefixParser.parse: term is not an array, number or string'); 118 } 119 120 method = term[0]; 121 le = term.length; 122 123 if (action === 'execute') { 124 if (Type.isInArray(['+', '-', '*', '/'], method)) { 125 126 res = this.parse(term[1], action); 127 for (i = 2; i < le; i++) { 128 v = this.parse(term[i], action); 129 switch (method) { 130 case '+': 131 res += v; 132 break; 133 case '-': 134 res -= v; 135 break; 136 case '*': 137 res *= v; 138 break; 139 case '/': 140 res /= v; 141 break; 142 default: 143 } 144 } 145 } else if (method === 'exec') { 146 fun = term[1]; 147 v = []; 148 for (i = 2; i < le; i++) { 149 v.push(this.parse(term[i], action)); 150 } 151 if (Type.exists(Math[fun])) { 152 res = Math[fun].apply(this, v); 153 } else if (Type.exists(Mat[fun])) { 154 res = Mat[fun].apply(this, v); 155 } else { 156 throw new Error("PrefixParser.parse: " + fun + " is not allowed"); 157 } 158 } else { 159 // Allow shortcut 'V' for 'Value' 160 fun = term[0]; 161 if (fun === 'V') { 162 fun = 'Value'; 163 } 164 165 if (!Type.exists(term[1][fun])) { 166 throw new Error("PrefixParser.parse: " + fun + " is not a method of " + term[1]); 167 } 168 v = []; 169 for (i = 2; i < le; i++) { 170 v.push(this.parse(term[i], action)); 171 } 172 res = term[1][fun].apply(term[1], v); 173 } 174 } 175 176 return res; 177 }, 178 179 /** 180 * Determine the dimension of the resulting value, i.e. ['L', obj] as well as 181 * ['+', ['L', obj1], ['L', obj2]] have dimension 1. 182 * <p> 183 * ['+', ['Area', obj1], ['L', obj2]] will retrun NaN, because the two 184 * operands have conflicting dimensions. 185 * <p> 186 * If an element is a measurement element, then it's dimension can be set as attribute. 187 * This overrules the computed dimension. 188 * 189 * @param {Array|Number} term Prefix expression 190 * @returns Number 191 */ 192 dimension: function (term) { 193 var method, i, le, res, fun, d, v, unit; 194 195 if (Type.isNumber(term)) { 196 return 0; 197 } 198 if (!Type.isArray(term) || term.length < 2) { 199 throw new Error('PrefixParser.dimension: term is not an array'); 200 } 201 202 method = term[0]; 203 le = term.length; 204 205 if (Type.isInArray(['+', '-', '*', '/'], method)) { 206 207 res = this.dimension(term[1]); 208 for (i = 2; i < le; i++) { 209 v = this.dimension(term[i]); 210 switch (method) { 211 case '+': 212 if (v !== res) { 213 res = NaN; 214 } 215 break; 216 case '-': 217 if (v !== res) { 218 res = NaN; 219 } 220 break; 221 case '*': 222 res += v; 223 break; 224 case '/': 225 res -= v; 226 break; 227 default: 228 } 229 } 230 231 } else if (method === 'exec') { 232 if (term[2].type === Type.OBJECT_TYPE_MEASUREMENT) { 233 res = term[2].Dimension(); 234 // If attribute "dim" is set, this overrules anything else. 235 if (Type.exists(term[2].visProp.dim)) { 236 d = Type.evaluate(term[2].visProp.dim); 237 if (d !== null) { 238 res = d; 239 } 240 } 241 } else { 242 res = 0; 243 } 244 } else { 245 // Allow shortcut 'V' for 'Value' 246 fun = term[0]; 247 248 switch (fun) { 249 case 'L': 250 case 'Length': 251 case 'Perimeter': 252 case 'Radius': 253 case 'R': 254 res = 1; 255 break; 256 case 'Area': 257 case 'A': 258 res = 2; 259 break; 260 default: // 'V', 'Value' 261 if (term[1].type === Type.OBJECT_TYPE_MEASUREMENT) { 262 res = term[1].Dimension(); 263 // If attribute "dim" is set, this overrules anything else. 264 if (Type.exists(term[1].visProp.dim)) { 265 d = Type.evaluate(term[1].visProp.dim); 266 if (d !== null) { 267 res = d; 268 } 269 } 270 } else { 271 res = 0; 272 273 if (fun === 'Value' || fun === 'V') { 274 // The Value method of sector, angle and arc does not have the same dimension 275 // for all units. 276 if ([Const.OBJECT_TYPE_ARC, Const.OBJECT_TYPE_SECTOR, Const.OBJECT_TYPE_ANGLE].indexOf(term[1].type) >= 0) { 277 unit = ''; 278 if (term.length === 3 && Type.isString(term[2])) { 279 unit = term[2].toLowerCase(); 280 } 281 if (unit === '') { 282 // Default values: 283 if (term[1].type === Const.OBJECT_TYPE_ANGLE) { 284 // Default for angle.Value() is radians, i.e. dim 0 285 res = 0; 286 } else { 287 // Default for sector|arc.Value() is length, i.e. dim 1 288 res = 1; 289 } 290 } else if (unit.indexOf('len') === 0) { 291 // Length has dim 1 292 res = 1; 293 } else { 294 // Angles in various units has dimension 0 295 res = 0; 296 } 297 } 298 } 299 } 300 } 301 } 302 303 return res; 304 }, 305 306 /** 307 * Convert a prefix expression into a new prefix expression in which 308 * JSXGraph elements have been replaced by their ids. 309 * 310 * @param {Array|Number} term 311 * @returns {Array|Number} 312 */ 313 toPrefix: function (term) { 314 var method, i, le, res; 315 316 if (Type.isNumber(term)) { 317 return term; 318 } 319 if (!Type.isArray(term) || term.length < 2) { 320 throw new Error('PrefixParser.toPrefix: term is not an array'); 321 } 322 323 method = term[0]; 324 le = term.length; 325 res = [method]; 326 327 for (i = 1; i < le; i++) { 328 if (Type.isInArray(['+', '-', '*', '/'], method)) { 329 res.push(this.toPrefix(term[i])); 330 } else { 331 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) { 332 res = term[i].toPrefix(); 333 } else if (method === 'exec') { 334 if (i === 1) { 335 res.push(term[i]); 336 } else { 337 res.push(this.toPrefix(term[i])); 338 } 339 } else { 340 res = [method, term[i].id]; 341 } 342 } 343 } 344 345 return res; 346 }, 347 348 /** 349 * Determine parent elements of a prefix expression. 350 * @param {Array|Number} term prefix expression 351 * @returns Array 352 * @private 353 */ 354 getParents: function (term) { 355 var method, i, le, res; 356 357 if (Type.isNumber(term)) { 358 return []; 359 } 360 if (!Type.isArray(term) || term.length < 2) { 361 throw new Error('PrefixParser.getParents: term is not an array'); 362 } 363 364 method = term[0]; 365 le = term.length; 366 res = []; 367 368 for (i = 1; i < le; i++) { 369 if (Type.isInArray(['+', '-', '*', '/'], method)) { 370 res = res.concat(this.getParents(term[i])); 371 } else { 372 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) { 373 res = res.concat(term[i].getParents()); 374 } else if (method === 'exec') { 375 if (i > 1) { 376 res = res.concat(this.getParents(term[i])); 377 } 378 } else { 379 res.push(term[i]); 380 } 381 } 382 } 383 384 return res; 385 } 386 }; 387 388 export default JXG.PrefixParser;