From cc0f277da7a5e8e636c11947ca3b05cbb0569a1f Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sun, 22 Sep 2024 05:57:36 +0200 Subject: [PATCH] Nearly rewrote expr-eval to be compatible as an ECMAScript module, the last blocker for many bugs and JS tests! commit f91538de446ef0e497ec7e87e2729f504a4172cb Author: Ad5001 Date: Sun Sep 22 05:55:12 2024 +0200 Converted expr-eval back to regular JS! commit 23c346f6c65b5b5c4bb4ad610f0554bd1d9a3700 Author: Ad5001 Date: Sun Sep 22 05:06:59 2024 +0200 Reformatting commit 66608c980fd44f26ae8e6855ecd5fc3e7db55a7b Author: Ad5001 Date: Sun Sep 22 05:04:27 2024 +0200 Removed all 'var's commit 545886fd38c99cf11bc576caa40bec0d7fe0ac30 Author: Ad5001 Date: Sun Sep 22 04:53:32 2024 +0200 Removing function definition commit 489602b24bb70cb6ad782871e269a22c92fcf072 Author: Ad5001 Date: Sun Sep 22 04:51:21 2024 +0200 Removing semicolons commit 3ee069edbeb8ebfb5c7d15d319014f7a085ff623 Author: Ad5001 Date: Sun Sep 22 04:49:32 2024 +0200 Converting all classes to ECMAScript classes --- .../LogarithmPlotter/LogarithmPlotter.qml | 2 +- .../eu/ad5001/LogarithmPlotter/js/autoload.js | 10 - .../ad5001/LogarithmPlotter/js/autoload.mjs | 27 + .../js/lib/expr-eval/expr-eval.js | 1852 ----------------- .../js/lib/expr-eval/expression.mjs | 540 +++++ .../js/lib/expr-eval/instruction.mjs | 82 + .../{integration.js => integration.mjs} | 49 +- .../js/lib/expr-eval/parser.mjs | 172 ++ .../js/lib/expr-eval/parserstate.mjs | 398 ++++ .../js/lib/expr-eval/polyfill.mjs | 371 ++++ .../js/lib/expr-eval/tokens.mjs | 575 +++++ .../ad5001/LogarithmPlotter/js/math/latex.mjs | 311 ++- 12 files changed, 2340 insertions(+), 2049 deletions(-) delete mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.js create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs delete mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expr-eval.js create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expression.mjs create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/instruction.mjs rename LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/{integration.js => integration.mjs} (68%) create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parser.mjs create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parserstate.mjs create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/polyfill.mjs create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/tokens.mjs diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml index 2e24d53..1a44958 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogarithmPlotter.qml @@ -23,7 +23,7 @@ import QtQuick.Layouts 1.12 import QtQuick // Auto loading all modules. -import "js/autoload.js" as ModulesAutoload +import "js/autoload.mjs" as ModulesAutoload import eu.ad5001.LogarithmPlotter.History 1.0 import eu.ad5001.LogarithmPlotter.ObjectLists 1.0 diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.js deleted file mode 100644 index 5763db0..0000000 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.js +++ /dev/null @@ -1,10 +0,0 @@ - -// Loading modules in order -.import "objects.mjs" as Objects -.import "lib/expr-eval/integration.js" as ExprParser -.import "objs/autoload.mjs" as Autoload -.import "math/latex.mjs" as Latex -.import "history/common.mjs" as HistoryCommon -.import "canvas.mjs" as CanvasAPI -.import "io.mjs" as IOAPI -.import "preferences.mjs" as PreferencesAPI \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs new file mode 100644 index 0000000..dcdaedb --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/autoload.mjs @@ -0,0 +1,27 @@ +/** + * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. + * Copyright (C) 2021-2024 Ad5001 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Loading modules in order +import * as Objects from "./objects.mjs" +import * as ExprParser from "./lib/expr-eval/integration.mjs" +import * as ObjsAutoload from "./objs/autoload.mjs" +import * as Latex from "./math/latex.mjs" +import * as HistoryCommon from "./history/common.mjs" +import * as CanvasAPI from "./canvas.mjs" +import * as IOAPI from "./io.mjs" +import * as PreferencesAPI from "./preferences.mjs" diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expr-eval.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expr-eval.js deleted file mode 100644 index 1afd66b..0000000 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expr-eval.js +++ /dev/null @@ -1,1852 +0,0 @@ -// https://silentmatt.com/javascript-expression-evaluator/ - -.pragma library - -var INUMBER = 'INUMBER'; -var IOP1 = 'IOP1'; -var IOP2 = 'IOP2'; -var IOP3 = 'IOP3'; -var IVAR = 'IVAR'; -var IVARNAME = 'IVARNAME'; -var IFUNCALL = 'IFUNCALL'; -var IFUNDEF = 'IFUNDEF'; -var IEXPR = 'IEXPR'; -var IEXPREVAL = 'IEXPREVAL'; -var IMEMBER = 'IMEMBER'; -var IENDSTATEMENT = 'IENDSTATEMENT'; -var IARRAY = 'IARRAY'; - -// Additional variable characters. -var ADDITIONAL_VARCHARS = [ - "α","β","γ","δ","ε","ζ","η", - "π","θ","κ","λ","μ","ξ","ρ", - "ς","σ","τ","φ","χ","ψ","ω", - "Γ","Δ","Θ","Λ","Ξ","Π","Σ", - "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", - "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", - "ₜ","¹","²","³","⁴","⁵","⁶", - "⁷","⁸","⁹","⁰","₁","₂","₃", - "₄","₅","₆","₇","₈","₉","₀", - "∞","π" -] - -function Instruction(type, value) { - this.type = type; - this.value = (value !== undefined && value !== null) ? value : 0; -} - -Instruction.prototype.toString = function () { - switch (this.type) { - case INUMBER: - case IOP1: - case IOP2: - case IOP3: - case IVAR: - case IVARNAME: - case IENDSTATEMENT: - return this.value; - case IFUNCALL: - return 'CALL ' + this.value; - case IFUNDEF: - return 'DEF ' + this.value; - case IARRAY: - return 'ARRAY ' + this.value; - case IMEMBER: - return '.' + this.value; - default: - return 'Invalid Instruction'; - } -}; - -function unaryInstruction(value) { - return new Instruction(IOP1, value); -} - -function binaryInstruction(value) { - return new Instruction(IOP2, value); -} - -function ternaryInstruction(value) { - return new Instruction(IOP3, value); -} - -function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) { - var nstack = []; - var newexpression = []; - var n1, n2, n3; - var f; - for (var i = 0; i < tokens.length; i++) { - var item = tokens[i]; - var type = item.type; - if (type === INUMBER || type === IVARNAME) { - if (Array.isArray(item.value)) { - nstack.push.apply(nstack, simplify(item.value.map(function (x) { - return new Instruction(INUMBER, x); - }).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values)); - } else { - nstack.push(item); - } - } else if (type === IVAR && values.hasOwnProperty(item.value)) { - item = new Instruction(INUMBER, values[item.value]); - nstack.push(item); - } else if (type === IOP2 && nstack.length > 1) { - n2 = nstack.pop(); - n1 = nstack.pop(); - f = binaryOps[item.value]; - item = new Instruction(INUMBER, f(n1.value, n2.value)); - nstack.push(item); - } else if (type === IOP3 && nstack.length > 2) { - n3 = nstack.pop(); - n2 = nstack.pop(); - n1 = nstack.pop(); - if (item.value === '?') { - nstack.push(n1.value ? n2.value : n3.value); - } else { - f = ternaryOps[item.value]; - item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value)); - nstack.push(item); - } - } else if (type === IOP1 && nstack.length > 0) { - n1 = nstack.pop(); - f = unaryOps[item.value]; - item = new Instruction(INUMBER, f(n1.value)); - nstack.push(item); - } else if (type === IEXPR) { - while (nstack.length > 0) { - newexpression.push(nstack.shift()); - } - newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values))); - } else if (type === IMEMBER && nstack.length > 0) { - n1 = nstack.pop(); - //console.log("Getting property ", item.value, "of", n1) - if(item.value in n1.value) - nstack.push(new Instruction(INUMBER, n1.value[item.value])); - else - throw new Error(qsTranslate('error', 'Cannot find property %1 of object %2.').arg(item.value).arg(n1)) - } /* else if (type === IARRAY && nstack.length >= item.value) { - var length = item.value; - while (length-- > 0) { - newexpression.push(nstack.pop()); - } - newexpression.push(new Instruction(IARRAY, item.value)); - } */ else { - while (nstack.length > 0) { - newexpression.push(nstack.shift()); - } - newexpression.push(item); - } - } - while (nstack.length > 0) { - newexpression.push(nstack.shift()); - } - return newexpression; -} - -function substitute(tokens, variable, expr) { - var newexpression = []; - for (var i = 0; i < tokens.length; i++) { - var item = tokens[i]; - var type = item.type; - if (type === IVAR && item.value === variable) { - for (var j = 0; j < expr.tokens.length; j++) { - var expritem = expr.tokens[j]; - var replitem; - if (expritem.type === IOP1) { - replitem = unaryInstruction(expritem.value); - } else if (expritem.type === IOP2) { - replitem = binaryInstruction(expritem.value); - } else if (expritem.type === IOP3) { - replitem = ternaryInstruction(expritem.value); - } else { - replitem = new Instruction(expritem.type, expritem.value); - } - newexpression.push(replitem); - } - } else if (type === IEXPR) { - newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr))); - } else { - newexpression.push(item); - } - } - return newexpression; -} - -function evaluate(tokens, expr, values) { - var nstack = []; - var n1, n2, n3; - var f, args, argCount; - - if (isExpressionEvaluator(tokens)) { - return resolveExpression(tokens, values); - } - - var numTokens = tokens.length; - - for (var i = 0; i < numTokens; i++) { - var item = tokens[i]; - var type = item.type; - if (type === INUMBER || type === IVARNAME) { - nstack.push(item.value); - } else if (type === IOP2) { - n2 = nstack.pop(); - n1 = nstack.pop(); - if (item.value === 'and') { - nstack.push(n1 ? !!evaluate(n2, expr, values) : false); - } else if (item.value === 'or') { - nstack.push(n1 ? true : !!evaluate(n2, expr, values)); - } else if (item.value === '=') { - f = expr.binaryOps[item.value]; - nstack.push(f(n1, evaluate(n2, expr, values), values)); - } else { - f = expr.binaryOps[item.value]; - nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))); - } - } else if (type === IOP3) { - n3 = nstack.pop(); - n2 = nstack.pop(); - n1 = nstack.pop(); - if (item.value === '?') { - nstack.push(evaluate(n1 ? n2 : n3, expr, values)); - } else { - f = expr.ternaryOps[item.value]; - nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values))); - } - } else if (type === IVAR) { - // Check for variable value - if (/^__proto__|prototype|constructor$/.test(item.value)) { - throw new Error('WARNING: Prototype access detected and denied. If you downloaded this file from the internet, this file might be a virus.'); - } else if (item.value in expr.functions) { - nstack.push(expr.functions[item.value]); - } else if (item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) { - nstack.push(expr.unaryOps[item.value]); - } else { - var v = values[item.value]; - if (v !== undefined) { - nstack.push(v); - } else { - throw new Error(qsTranslate('error', 'Undefined variable %1.').arg(item.value)); - } - } - } else if (type === IOP1) { - n1 = nstack.pop(); - f = expr.unaryOps[item.value]; - nstack.push(f(resolveExpression(n1, values))); - } else if (type === IFUNCALL) { - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(resolveExpression(nstack.pop(), values)); - } - f = nstack.pop(); - if (f.apply && f.call) { - nstack.push(f.apply(undefined, args)); - } else if(f.execute) { - // Objects & expressions execution - if(args.length >= 1) - nstack.push(f.execute.apply(f, args)); - else - throw new Error(qsTranslate('error', 'In order to be executed, object %1 must have at least one argument.').arg(f)) - } else { - throw new Error(qsTranslate('error', '%1 cannot be executed.').arg(f)); - } - } else if (type === IFUNDEF) { - // Create closure to keep references to arguments and expression - nstack.push((function () { - var n2 = nstack.pop(); - var args = []; - var argCount = item.value; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - var n1 = nstack.pop(); - var f = function () { - var scope = Object.assign({}, values); - for (var i = 0, len = args.length; i < len; i++) { - scope[args[i]] = arguments[i]; - } - return evaluate(n2, expr, scope); - }; - // f.name = n1 - Object.defineProperty(f, 'name', { - value: n1, - writable: false - }); - values[n1] = f; - return f; - })()); - } else if (type === IEXPR) { - nstack.push(createExpressionEvaluator(item, expr)); - } else if (type === IEXPREVAL) { - nstack.push(item); - } else if (type === IMEMBER) { - n1 = nstack.pop(); - //console.log("Getting property", item.value, "of", n1,":",n1[item.value]) - if(item.value in n1) - if(n1[item.value].execute && n1[item.value].cached) - nstack.push(n1[item.value].execute()) - else - nstack.push(n1[item.value]); - else - throw new Error(qsTranslate('error', 'Cannot find property %1 of object %2.').arg(item.value).arg(n1)) - } else if (type === IENDSTATEMENT) { - nstack.pop(); - } else if (type === IARRAY) { - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - nstack.push(args); - } else { - throw new Error(qsTranslate('error', 'Invalid expression.')); - } - } - if (nstack.length > 1) { - throw new Error(qsTranslate('error', 'Invalid expression (parity).')); - } - // Explicitly return zero to avoid test issues caused by -0 - return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values); -} - -function createExpressionEvaluator(token, expr, values) { - if (isExpressionEvaluator(token)) return token; - return { - type: IEXPREVAL, - value: function (scope) { - return evaluate(token.value, expr, scope); - } - }; -} - -function isExpressionEvaluator(n) { - return n && n.type === IEXPREVAL; -} - -function resolveExpression(n, values) { - return isExpressionEvaluator(n) ? n.value(values) : n; -} - -function expressionToString(tokens, toJS) { - var nstack = []; - var n1, n2, n3; - var f, args, argCount; - for (var i = 0; i < tokens.length; i++) { - var item = tokens[i]; - var type = item.type; - if (type === INUMBER) { - if (typeof item.value === 'number' && item.value < 0) { - nstack.push('(' + item.value + ')'); - } else if (Array.isArray(item.value)) { - nstack.push('[' + item.value.map(escapeValue).join(', ') + ']'); - } else { - nstack.push(escapeValue(item.value)); - } - } else if (type === IOP2) { - n2 = nstack.pop(); - n1 = nstack.pop(); - f = item.value; - if (toJS) { - if (f === '^') { - nstack.push('Math.pow(' + n1 + ', ' + n2 + ')'); - } else if (f === 'and') { - nstack.push('(!!' + n1 + ' && !!' + n2 + ')'); - } else if (f === 'or') { - nstack.push('(!!' + n1 + ' || !!' + n2 + ')'); - } else if (f === '||') { - nstack.push('(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((' + n1 + '),(' + n2 + ')))'); - } else if (f === '==') { - nstack.push('(' + n1 + ' === ' + n2 + ')'); - } else if (f === '!=') { - nstack.push('(' + n1 + ' !== ' + n2 + ')'); - } else if (f === '[') { - nstack.push(n1 + '[(' + n2 + ') | 0]'); - } else { - nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); - } - } else { - if (f === '[') { - nstack.push(n1 + '[' + n2 + ']'); - } else { - nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); - } - } - } else if (type === IOP3) { - n3 = nstack.pop(); - n2 = nstack.pop(); - n1 = nstack.pop(); - f = item.value; - if (f === '?') { - nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')'); - } else { - throw new Error(qsTranslate('error', 'Invalid expression.')); - } - } else if (type === IVAR || type === IVARNAME) { - nstack.push(item.value); - } else if (type === IOP1) { - n1 = nstack.pop(); - f = item.value; - if (f === '-' || f === '+') { - nstack.push('(' + f + n1 + ')'); - } else if (toJS) { - if (f === 'not') { - nstack.push('(' + '!' + n1 + ')'); - } else if (f === '!') { - nstack.push('fac(' + n1 + ')'); - } else { - nstack.push(f + '(' + n1 + ')'); - } - } else if (f === '!') { - nstack.push('(' + n1 + '!)'); - } else { - nstack.push('(' + f + ' ' + n1 + ')'); - } - } else if (type === IFUNCALL) { - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - f = nstack.pop(); - nstack.push(f + '(' + args.join(', ') + ')'); - } else if (type === IFUNDEF) { - n2 = nstack.pop(); - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - n1 = nstack.pop(); - if (toJS) { - nstack.push('(' + n1 + ' = function(' + args.join(', ') + ') { return ' + n2 + ' })'); - } else { - nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')'); - } - } else if (type === IMEMBER) { - n1 = nstack.pop(); - nstack.push(n1 + '.' + item.value); - } else if (type === IARRAY) { - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); - } - nstack.push('[' + args.join(', ') + ']'); - } else if (type === IEXPR) { - nstack.push('(' + expressionToString(item.value, toJS) + ')'); - } else if (type === IENDSTATEMENT) ; else { - throw new Error(qsTranslate('error', 'Invalid expression.')); - } - } - if (nstack.length > 1) { - if (toJS) { - nstack = [ nstack.join(',') ]; - } else { - nstack = [ nstack.join(';') ]; - } - } - return String(nstack[0]); -} - -function escapeValue(v) { - if (typeof v === 'string') { - return JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); - } - return v; -} - -function contains(array, obj) { - for (var i = 0; i < array.length; i++) { - if (array[i] === obj) { - return true; - } - } - return false; -} - -function getSymbols(tokens, symbols, options) { - options = options || {}; - var withMembers = !!options.withMembers; - var prevVar = null; - - for (var i = 0; i < tokens.length; i++) { - var item = tokens[i]; - if (item.type === IVAR || item.type === IVARNAME) { - if (!withMembers && !contains(symbols, item.value)) { - symbols.push(item.value); - } else if (prevVar !== null) { - if (!contains(symbols, prevVar)) { - symbols.push(prevVar); - } - prevVar = item.value; - } else { - prevVar = item.value; - } - } else if (item.type === IMEMBER && withMembers && prevVar !== null) { - prevVar += '.' + item.value; - } else if (item.type === IEXPR) { - getSymbols(item.value, symbols, options); - } else if (prevVar !== null) { - if (!contains(symbols, prevVar)) { - symbols.push(prevVar); - } - prevVar = null; - } - } - - if (prevVar !== null && !contains(symbols, prevVar)) { - symbols.push(prevVar); - } -} - -function Expression(tokens, parser) { - this.tokens = tokens; - this.parser = parser; - this.unaryOps = parser.unaryOps; - this.binaryOps = parser.binaryOps; - this.ternaryOps = parser.ternaryOps; - this.functions = parser.functions; -} - -Expression.prototype.simplify = function (values) { - values = values || {}; - return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser); -}; - -Expression.prototype.substitute = function (variable, expr) { - if (!(expr instanceof Expression)) { - expr = this.parser.parse(String(expr)); - } - - return new Expression(substitute(this.tokens, variable, expr), this.parser); -}; - -Expression.prototype.evaluate = function (values) { - values = Object.assign({}, values, this.parser.consts) - return evaluate(this.tokens, this, values); -}; - -Expression.prototype.toString = function () { - return expressionToString(this.tokens, false); -}; - -Expression.prototype.symbols = function (options) { - options = options || {}; - var vars = []; - getSymbols(this.tokens, vars, options); - return vars; -}; - -Expression.prototype.variables = function (options) { - options = options || {}; - var vars = []; - getSymbols(this.tokens, vars, options); - var functions = this.functions; - var consts = this.parser.consts - return vars.filter(function (name) { - return !(name in functions) && !(name in consts); - }); -}; - -Expression.prototype.toJSFunction = function (param, variables) { - var expr = this; - var f = new Function(param, 'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' + expressionToString(this.simplify(variables).tokens, true) + '; }'); // eslint-disable-line no-new-func - return function () { - return f.apply(expr, arguments); - }; -}; - -var TEOF = 'TEOF'; -var TOP = 'TOP'; -var TNUMBER = 'TNUMBER'; -var TSTRING = 'TSTRING'; -var TPAREN = 'TPAREN'; -var TBRACKET = 'TBRACKET'; -var TCOMMA = 'TCOMMA'; -var TNAME = 'TNAME'; -var TSEMICOLON = 'TSEMICOLON'; - -function Token(type, value, index) { - this.type = type; - this.value = value; - this.index = index; -} - -Token.prototype.toString = function () { - return this.type + ': ' + this.value; -}; - -function TokenStream(parser, expression) { - this.pos = 0; - this.current = null; - this.unaryOps = parser.unaryOps; - this.binaryOps = parser.binaryOps; - this.ternaryOps = parser.ternaryOps; - this.builtinConsts = parser.builtinConsts; - this.expression = expression; - this.savedPosition = 0; - this.savedCurrent = null; - this.options = parser.options; - this.parser = parser; -} - -TokenStream.prototype.newToken = function (type, value, pos) { - return new Token(type, value, pos != null ? pos : this.pos); -}; - -TokenStream.prototype.save = function () { - this.savedPosition = this.pos; - this.savedCurrent = this.current; -}; - -TokenStream.prototype.restore = function () { - this.pos = this.savedPosition; - this.current = this.savedCurrent; -}; - -TokenStream.prototype.next = function () { - if (this.pos >= this.expression.length) { - return this.newToken(TEOF, 'EOF'); - } - - if (this.isWhitespace() || this.isComment()) { - return this.next(); - } else if (this.isRadixInteger() || - this.isNumber() || - this.isOperator() || - this.isString() || - this.isParen() || - this.isBracket() || - this.isComma() || - this.isSemicolon() || - this.isNamedOp() || - this.isConst() || - this.isName()) { - return this.current; - } else { - this.parseError(qsTranslate('error', 'Unknown character "%1".').arg(this.expression.charAt(this.pos))); - } -}; - -TokenStream.prototype.isString = function () { - var r = false; - var startPos = this.pos; - var quote = this.expression.charAt(startPos); - - if (quote === '\'' || quote === '"') { - var index = this.expression.indexOf(quote, startPos + 1); - while (index >= 0 && this.pos < this.expression.length) { - this.pos = index + 1; - if (this.expression.charAt(index - 1) !== '\\') { - var rawString = this.expression.substring(startPos + 1, index); - this.current = this.newToken(TSTRING, this.unescape(rawString), startPos); - r = true; - break; - } - index = this.expression.indexOf(quote, index + 1); - } - } - return r; -}; - -TokenStream.prototype.isParen = function () { - var c = this.expression.charAt(this.pos); - if (c === '(' || c === ')') { - this.current = this.newToken(TPAREN, c); - this.pos++; - return true; - } - return false; -}; - -TokenStream.prototype.isBracket = function () { - var c = this.expression.charAt(this.pos); - if ((c === '[' || c === ']') && this.isOperatorEnabled('[')) { - this.current = this.newToken(TBRACKET, c); - this.pos++; - return true; - } - return false; -}; - -TokenStream.prototype.isComma = function () { - var c = this.expression.charAt(this.pos); - if (c === ',') { - this.current = this.newToken(TCOMMA, ','); - this.pos++; - return true; - } - return false; -}; - -TokenStream.prototype.isSemicolon = function () { - var c = this.expression.charAt(this.pos); - if (c === ';') { - this.current = this.newToken(TSEMICOLON, ';'); - this.pos++; - return true; - } - return false; -}; - -TokenStream.prototype.isConst = function () { - var startPos = this.pos; - var i = startPos; - for (; i < this.expression.length; i++) { - var c = this.expression.charAt(i); - if (c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) { - if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) { - break; - } - } - } - if (i > startPos) { - var str = this.expression.substring(startPos, i); - if (str in this.builtinConsts) { - this.current = this.newToken(TNUMBER, this.builtinConsts[str]); - this.pos += str.length; - return true; - } - } - return false; -}; - -TokenStream.prototype.isNamedOp = function () { - var startPos = this.pos; - var i = startPos; - for (; i < this.expression.length; i++) { - var c = this.expression.charAt(i); - if (c.toUpperCase() === c.toLowerCase()) { - if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) { - break; - } - } - } - if (i > startPos) { - var str = this.expression.substring(startPos, i); - if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) { - this.current = this.newToken(TOP, str); - this.pos += str.length; - return true; - } - } - return false; -}; - -TokenStream.prototype.isName = function () { - var startPos = this.pos; - var i = startPos; - var hasLetter = false; - for (; i < this.expression.length; i++) { - var c = this.expression.charAt(i); - if (c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) { - if (i === this.pos && (c === '$' || c === '_')) { - if (c === '_') { - hasLetter = true; - } - continue; - } else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) { - break; - } - } else { - hasLetter = true; - } - } - if (hasLetter) { - var str = this.expression.substring(startPos, i); - this.current = this.newToken(TNAME, str); - this.pos += str.length; - return true; - } - return false; -}; - -TokenStream.prototype.isWhitespace = function () { - var r = false; - var c = this.expression.charAt(this.pos); - while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { - r = true; - this.pos++; - if (this.pos >= this.expression.length) { - break; - } - c = this.expression.charAt(this.pos); - } - return r; -}; - -var codePointPattern = /^[0-9a-f]{4}$/i; - -TokenStream.prototype.unescape = function (v) { - var index = v.indexOf('\\'); - if (index < 0) { - return v; - } - - var buffer = v.substring(0, index); - while (index >= 0) { - var c = v.charAt(++index); - switch (c) { - case '\'': - buffer += '\''; - break; - case '"': - buffer += '"'; - break; - case '\\': - buffer += '\\'; - break; - case '/': - buffer += '/'; - break; - case 'b': - buffer += '\b'; - break; - case 'f': - buffer += '\f'; - break; - case 'n': - buffer += '\n'; - break; - case 'r': - buffer += '\r'; - break; - case 't': - buffer += '\t'; - break; - case 'u': - // interpret the following 4 characters as the hex of the unicode code point - var codePoint = v.substring(index + 1, index + 5); - if (!codePointPattern.test(codePoint)) { - this.parseError(qsTranslate('error', 'Illegal escape sequence: %1.').arg("\\u" + codePoint)); - } - buffer += String.fromCharCode(parseInt(codePoint, 16)); - index += 4; - break; - default: - throw this.parseError(qsTranslate('error', 'Illegal escape sequence: %1.').arg('\\' + c)); - } - ++index; - var backslash = v.indexOf('\\', index); - buffer += v.substring(index, backslash < 0 ? v.length : backslash); - index = backslash; - } - - return buffer; -}; - -TokenStream.prototype.isComment = function () { - var c = this.expression.charAt(this.pos); - if (c === '/' && this.expression.charAt(this.pos + 1) === '*') { - this.pos = this.expression.indexOf('*/', this.pos) + 2; - if (this.pos === 1) { - this.pos = this.expression.length; - } - return true; - } - return false; -}; - -TokenStream.prototype.isRadixInteger = function () { - var pos = this.pos; - - if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') { - return false; - } - ++pos; - - var radix; - var validDigit; - if (this.expression.charAt(pos) === 'x') { - radix = 16; - validDigit = /^[0-9a-f]$/i; - ++pos; - } else if (this.expression.charAt(pos) === 'b') { - radix = 2; - validDigit = /^[01]$/i; - ++pos; - } else { - return false; - } - - var valid = false; - var startPos = pos; - - while (pos < this.expression.length) { - var c = this.expression.charAt(pos); - if (validDigit.test(c)) { - pos++; - valid = true; - } else { - break; - } - } - - if (valid) { - this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix)); - this.pos = pos; - } - return valid; -}; - -TokenStream.prototype.isNumber = function () { - var valid = false; - var pos = this.pos; - var startPos = pos; - var resetPos = pos; - var foundDot = false; - var foundDigits = false; - var c; - - while (pos < this.expression.length) { - c = this.expression.charAt(pos); - if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) { - if (c === '.') { - foundDot = true; - } else { - foundDigits = true; - } - pos++; - valid = foundDigits; - } else { - break; - } - } - - if (valid) { - resetPos = pos; - } - - if (c === 'e' || c === 'E') { - pos++; - var acceptSign = true; - var validExponent = false; - while (pos < this.expression.length) { - c = this.expression.charAt(pos); - if (acceptSign && (c === '+' || c === '-')) { - acceptSign = false; - } else if (c >= '0' && c <= '9') { - validExponent = true; - acceptSign = false; - } else { - break; - } - pos++; - } - - if (!validExponent) { - pos = resetPos; - } - } - - if (valid) { - this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos))); - this.pos = pos; - } else { - this.pos = resetPos; - } - return valid; -}; - -TokenStream.prototype.isOperator = function () { - var startPos = this.pos; - var c = this.expression.charAt(this.pos); - - if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === '?' || c === ':' || c === '.') { - this.current = this.newToken(TOP, c); - } else if (c === '∙' || c === '•') { - this.current = this.newToken(TOP, '*'); - } else if (c === '>') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '>='); - this.pos++; - } else { - this.current = this.newToken(TOP, '>'); - } - } else if (c === '<') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '<='); - this.pos++; - } else { - this.current = this.newToken(TOP, '<'); - } - } else if (c === '|') { - if (this.expression.charAt(this.pos + 1) === '|') { - this.current = this.newToken(TOP, '||'); - this.pos++; - } else { - return false; - } - } else if (c === '=') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '=='); - this.pos++; - } else { - this.current = this.newToken(TOP, c); - } - } else if (c === '!') { - if (this.expression.charAt(this.pos + 1) === '=') { - this.current = this.newToken(TOP, '!='); - this.pos++; - } else { - this.current = this.newToken(TOP, c); - } - } else { - return false; - } - this.pos++; - - if (this.isOperatorEnabled(this.current.value)) { - return true; - } else { - this.pos = startPos; - return false; - } -}; - -TokenStream.prototype.isOperatorEnabled = function (op) { - return this.parser.isOperatorEnabled(op); -}; - -TokenStream.prototype.getCoordinates = function () { - var line = 0; - var column; - var newline = -1; - do { - line++; - column = this.pos - newline; - newline = this.expression.indexOf('\n', newline + 1); - } while (newline >= 0 && newline < this.pos); - - return { - line: line, - column: column - }; -}; - -TokenStream.prototype.parseError = function (msg) { - var coords = this.getCoordinates(); - throw new Error(qsTranslate('error', 'Parse error [%1:%2]: %3').arg(coords.line).arg(coords.column).arg(msg)); -}; - -function ParserState(parser, tokenStream, options) { - this.parser = parser; - this.tokens = tokenStream; - this.current = null; - this.nextToken = null; - this.next(); - this.savedCurrent = null; - this.savedNextToken = null; - this.allowMemberAccess = options.allowMemberAccess !== false; -} - -ParserState.prototype.next = function () { - this.current = this.nextToken; - return (this.nextToken = this.tokens.next()); -}; - -ParserState.prototype.tokenMatches = function (token, value) { - if (typeof value === 'undefined') { - return true; - } else if (Array.isArray(value)) { - return contains(value, token.value); - } else if (typeof value === 'function') { - return value(token); - } else { - return token.value === value; - } -}; - -ParserState.prototype.save = function () { - this.savedCurrent = this.current; - this.savedNextToken = this.nextToken; - this.tokens.save(); -}; - -ParserState.prototype.restore = function () { - this.tokens.restore(); - this.current = this.savedCurrent; - this.nextToken = this.savedNextToken; -}; - -ParserState.prototype.accept = function (type, value) { - if (this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) { - this.next(); - return true; - } - return false; -}; - -ParserState.prototype.expect = function (type, value) { - if (!this.accept(type, value)) { - var coords = this.tokens.getCoordinates(); - throw new Error(qsTranslate('error', 'Parse error [%1:%2]: %3') - .arg(coords.line).arg(coords.column) - .arg(qsTranslate('error', 'Expected %1').arg(value || type))); - } -}; - -ParserState.prototype.parseAtom = function (instr) { - var unaryOps = this.tokens.unaryOps; - function isPrefixOperator(token) { - return token.value in unaryOps; - } - - if (this.accept(TNAME) || this.accept(TOP, isPrefixOperator)) { - instr.push(new Instruction(IVAR, this.current.value)); - } else if (this.accept(TNUMBER)) { - instr.push(new Instruction(INUMBER, this.current.value)); - } else if (this.accept(TSTRING)) { - instr.push(new Instruction(INUMBER, this.current.value)); - } else if (this.accept(TPAREN, '(')) { - this.parseExpression(instr); - this.expect(TPAREN, ')'); - } else if (this.accept(TBRACKET, '[')) { - if (this.accept(TBRACKET, ']')) { - instr.push(new Instruction(IARRAY, 0)); - } else { - var argCount = this.parseArrayList(instr); - instr.push(new Instruction(IARRAY, argCount)); - } - } else { - throw new Error(qsTranslate('error', 'Unexpected %1').arg(this.nextToken)); - } -}; - -ParserState.prototype.parseExpression = function (instr) { - var exprInstr = []; - if (this.parseUntilEndStatement(instr, exprInstr)) { - return; - } - this.parseConditionalExpression(exprInstr); - if (this.parseUntilEndStatement(instr, exprInstr)) { - return; - } - this.pushExpression(instr, exprInstr); -}; - -ParserState.prototype.pushExpression = function (instr, exprInstr) { - for (var i = 0, len = exprInstr.length; i < len; i++) { - instr.push(exprInstr[i]); - } -}; - -ParserState.prototype.parseUntilEndStatement = function (instr, exprInstr) { - if (!this.accept(TSEMICOLON)) return false; - if (this.nextToken && this.nextToken.type !== TEOF && !(this.nextToken.type === TPAREN && this.nextToken.value === ')')) { - exprInstr.push(new Instruction(IENDSTATEMENT)); - } - if (this.nextToken.type !== TEOF) { - this.parseExpression(exprInstr); - } - instr.push(new Instruction(IEXPR, exprInstr)); - return true; -}; - -ParserState.prototype.parseArrayList = function (instr) { - var argCount = 0; - - while (!this.accept(TBRACKET, ']')) { - this.parseExpression(instr); - ++argCount; - while (this.accept(TCOMMA)) { - this.parseExpression(instr); - ++argCount; - } - } - - return argCount; -}; - -ParserState.prototype.parseConditionalExpression = function (instr) { - this.parseOrExpression(instr); - while (this.accept(TOP, '?')) { - var trueBranch = []; - var falseBranch = []; - this.parseConditionalExpression(trueBranch); - this.expect(TOP, ':'); - this.parseConditionalExpression(falseBranch); - instr.push(new Instruction(IEXPR, trueBranch)); - instr.push(new Instruction(IEXPR, falseBranch)); - instr.push(ternaryInstruction('?')); - } -}; - -ParserState.prototype.parseOrExpression = function (instr) { - this.parseAndExpression(instr); - while (this.accept(TOP, 'or')) { - var falseBranch = []; - this.parseAndExpression(falseBranch); - instr.push(new Instruction(IEXPR, falseBranch)); - instr.push(binaryInstruction('or')); - } -}; - -ParserState.prototype.parseAndExpression = function (instr) { - this.parseComparison(instr); - while (this.accept(TOP, 'and')) { - var trueBranch = []; - this.parseComparison(trueBranch); - instr.push(new Instruction(IEXPR, trueBranch)); - instr.push(binaryInstruction('and')); - } -}; - -var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'in']; - -ParserState.prototype.parseComparison = function (instr) { - this.parseAddSub(instr); - while (this.accept(TOP, COMPARISON_OPERATORS)) { - var op = this.current; - this.parseAddSub(instr); - instr.push(binaryInstruction(op.value)); - } -}; - -var ADD_SUB_OPERATORS = ['+', '-', '||']; - -ParserState.prototype.parseAddSub = function (instr) { - this.parseTerm(instr); - while (this.accept(TOP, ADD_SUB_OPERATORS)) { - var op = this.current; - this.parseTerm(instr); - instr.push(binaryInstruction(op.value)); - } -}; - -var TERM_OPERATORS = ['*', '/', '%']; - -ParserState.prototype.parseTerm = function (instr) { - this.parseFactor(instr); - while (this.accept(TOP, TERM_OPERATORS)) { - var op = this.current; - this.parseFactor(instr); - instr.push(binaryInstruction(op.value)); - } -}; - -ParserState.prototype.parseFactor = function (instr) { - var unaryOps = this.tokens.unaryOps; - function isPrefixOperator(token) { - return token.value in unaryOps; - } - - this.save(); - if (this.accept(TOP, isPrefixOperator)) { - if (this.current.value !== '-' && this.current.value !== '+') { - if (this.nextToken.type === TPAREN && this.nextToken.value === '(') { - this.restore(); - this.parseExponential(instr); - return; - } else if (this.nextToken.type === TSEMICOLON || this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ')')) { - this.restore(); - this.parseAtom(instr); - return; - } - } - - var op = this.current; - this.parseFactor(instr); - instr.push(unaryInstruction(op.value)); - } else { - this.parseExponential(instr); - } -}; - -ParserState.prototype.parseExponential = function (instr) { - this.parsePostfixExpression(instr); - while (this.accept(TOP, '^')) { - this.parseFactor(instr); - instr.push(binaryInstruction('^')); - } -}; - -ParserState.prototype.parsePostfixExpression = function (instr) { - this.parseFunctionCall(instr); - while (this.accept(TOP, '!')) { - instr.push(unaryInstruction('!')); - } -}; - -ParserState.prototype.parseFunctionCall = function (instr) { - var unaryOps = this.tokens.unaryOps; - function isPrefixOperator(token) { - return token.value in unaryOps; - } - - if (this.accept(TOP, isPrefixOperator)) { - var op = this.current; - this.parseAtom(instr); - instr.push(unaryInstruction(op.value)); - } else { - this.parseMemberExpression(instr); - while (this.accept(TPAREN, '(')) { - if (this.accept(TPAREN, ')')) { - instr.push(new Instruction(IFUNCALL, 0)); - } else { - var argCount = this.parseArgumentList(instr); - instr.push(new Instruction(IFUNCALL, argCount)); - } - } - } -}; - -ParserState.prototype.parseArgumentList = function (instr) { - var argCount = 0; - - while (!this.accept(TPAREN, ')')) { - this.parseExpression(instr); - ++argCount; - while (this.accept(TCOMMA)) { - this.parseExpression(instr); - ++argCount; - } - } - - return argCount; -}; - -ParserState.prototype.parseMemberExpression = function (instr) { - this.parseAtom(instr); - while (this.accept(TOP, '.') || this.accept(TBRACKET, '[')) { - var op = this.current; - - if (op.value === '.') { - if (!this.allowMemberAccess) { - throw new Error(qsTranslate('error', 'Unexpected ".": member access is not permitted')); - } - - this.expect(TNAME); - instr.push(new Instruction(IMEMBER, this.current.value)); - } else if (op.value === '[') { - if (!this.tokens.isOperatorEnabled('[')) { - throw new Error(qsTranslate('error', 'Unexpected "[]": arrays are disabled.')); - } - - this.parseExpression(instr); - this.expect(TBRACKET, ']'); - instr.push(binaryInstruction('[')); - } else { - throw new Error(qsTranslate('error', 'Unexpected symbol: %1.').arg(op.value)); - } - } -}; - -function add(a, b) { - return Number(a) + Number(b); -} - -function sub(a, b) { - return a - b; -} - -function mul(a, b) { - return a * b; -} - -function div(a, b) { - return a / b; -} - -function mod(a, b) { - return a % b; -} - -function concat(a, b) { - if (Array.isArray(a) && Array.isArray(b)) { - return a.concat(b); - } - return '' + a + b; -} - -function equal(a, b) { - return a === b; -} - -function notEqual(a, b) { - return a !== b; -} - -function greaterThan(a, b) { - return a > b; -} - -function lessThan(a, b) { - return a < b; -} - -function greaterThanEqual(a, b) { - return a >= b; -} - -function lessThanEqual(a, b) { - return a <= b; -} - -function andOperator(a, b) { - return Boolean(a && b); -} - -function orOperator(a, b) { - return Boolean(a || b); -} - -function inOperator(a, b) { - return contains(b, a); -} - -function sinh(a) { - return ((Math.exp(a) - Math.exp(-a)) / 2); -} - -function cosh(a) { - return ((Math.exp(a) + Math.exp(-a)) / 2); -} - -function tanh(a) { - if (a === Infinity) return 1; - if (a === -Infinity) return -1; - return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)); -} - -function asinh(a) { - if (a === -Infinity) return a; - return Math.log(a + Math.sqrt((a * a) + 1)); -} - -function acosh(a) { - return Math.log(a + Math.sqrt((a * a) - 1)); -} - -function atanh(a) { - return (Math.log((1 + a) / (1 - a)) / 2); -} - -function log10(a) { - return Math.log(a) * Math.LOG10E; -} - -function neg(a) { - return -a; -} - -function not(a) { - return !a; -} - -function trunc(a) { - return a < 0 ? Math.ceil(a) : Math.floor(a); -} - -function random(a) { - return Math.random() * (a || 1); -} - -function factorial(a) { // a! - return gamma(a + 1); -} - -function isInteger(value) { - return isFinite(value) && (value === Math.round(value)); -} - -var GAMMA_G = 4.7421875; -var GAMMA_P = [ - 0.99999999999999709182, - 57.156235665862923517, -59.597960355475491248, - 14.136097974741747174, -0.49191381609762019978, - 0.33994649984811888699e-4, - 0.46523628927048575665e-4, -0.98374475304879564677e-4, - 0.15808870322491248884e-3, -0.21026444172410488319e-3, - 0.21743961811521264320e-3, -0.16431810653676389022e-3, - 0.84418223983852743293e-4, -0.26190838401581408670e-4, - 0.36899182659531622704e-5 -]; - -// Gamma function from math.js -function gamma(n) { - var t, x; - - if (isInteger(n)) { - if (n <= 0) { - return isFinite(n) ? Infinity : NaN; - } - - if (n > 171) { - return Infinity; // Will overflow - } - - var value = n - 2; - var res = n - 1; - while (value > 1) { - res *= value; - value--; - } - - if (res === 0) { - res = 1; // 0! is per definition 1 - } - - return res; - } - - if (n < 0.5) { - return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n)); - } - - if (n >= 171.35) { - return Infinity; // will overflow - } - - if (n > 85.0) { // Extended Stirling Approx - var twoN = n * n; - var threeN = twoN * n; - var fourN = threeN * n; - var fiveN = fourN * n; - return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) * - (1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) - - (571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) + - (5246819 / (75246796800 * fiveN * n))); - } - - --n; - x = GAMMA_P[0]; - for (var i = 1; i < GAMMA_P.length; ++i) { - x += GAMMA_P[i] / (n + i); - } - - t = n + GAMMA_G + 0.5; - return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x; -} - -function stringOrArrayLength(s) { - if (Array.isArray(s)) { - return s.length; - } - return String(s).length; -} - -function hypot() { - var sum = 0; - var larg = 0; - for (var i = 0; i < arguments.length; i++) { - var arg = Math.abs(arguments[i]); - var div; - if (larg < arg) { - div = larg / arg; - sum = (sum * div * div) + 1; - larg = arg; - } else if (arg > 0) { - div = arg / larg; - sum += div * div; - } else { - sum += arg; - } - } - return larg === Infinity ? Infinity : larg * Math.sqrt(sum); -} - -function condition(cond, yep, nope) { - return cond ? yep : nope; -} - -/** -* Decimal adjustment of a number. -* From @escopecz. -* -* @param {Number} value The number. -* @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). -* @return {Number} The adjusted value. -*/ -function roundTo(value, exp) { - // If the exp is undefined or zero... - if (typeof exp === 'undefined' || +exp === 0) { - return Math.round(value); - } - value = +value; - exp = -(+exp); - // If the value is not a number or the exp is not an integer... - if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { - return NaN; - } - // Shift - value = value.toString().split('e'); - value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); - // Shift back - value = value.toString().split('e'); - return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); -} - -function setVar(name, value, variables) { - if (variables) variables[name] = value; - return value; -} - -function arrayIndex(array, index) { - return array[index | 0]; -} - -function max(array) { - if (arguments.length === 1 && Array.isArray(array)) { - return Math.max.apply(Math, array); - } else if(arguments.length >= 1) { - return Math.max.apply(Math, arguments); - } else { - throw new EvalError(qsTranslate('error', 'Function %1 must have at least one argument.').arg('max')) - } -} - -function min(array) { - if (arguments.length === 1 && Array.isArray(array)) { - return Math.min.apply(Math, array); - } else if(arguments.length >= 1) { - return Math.min.apply(Math, arguments); - } else { - throw new EvalError(qsTranslate('error', 'Function %1 must have at least one argument.').arg('min')) - } -} - -function arrayMap(f, a) { - if (typeof f !== 'function') { - throw new EvalError(qsTranslate('error', 'First argument to map is not a function.')); - } - if (!Array.isArray(a)) { - throw new EvalError(qsTranslate('error', 'Second argument to map is not an array.')); - } - return a.map(function (x, i) { - return f(x, i); - }); -} - -function arrayFold(f, init, a) { - if (typeof f !== 'function') { - throw new EvalError(qsTranslate('error', 'First argument to fold is not a function.')); - } - if (!Array.isArray(a)) { - throw new EvalError(qsTranslate('error', 'Second argument to fold is not an array.')); - } - return a.reduce(function (acc, x, i) { - return f(acc, x, i); - }, init); -} - -function arrayFilter(f, a) { - if (typeof f !== 'function') { - throw new EvalError(qsTranslate('error', 'First argument to filter is not a function.')); - } - if (!Array.isArray(a)) { - throw new EvalError(qsTranslate('error', 'Second argument to filter is not an array.')); - } - return a.filter(function (x, i) { - return f(x, i); - }); -} - -function stringOrArrayIndexOf(target, s) { - if (!(Array.isArray(s) || typeof s === 'string')) { - throw new Error(qsTranslate('error', 'Second argument to indexOf is not a string or array.')); - } - - return s.indexOf(target); -} - -function arrayJoin(sep, a) { - if (!Array.isArray(a)) { - throw new Error(qsTranslate('error', 'Second argument to join is not an array.')); - } - - return a.join(sep); -} - -function sign(x) { - return ((x > 0) - (x < 0)) || +x; -} - -var ONE_THIRD = 1/3; -function cbrt(x) { - return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD); -} - -function expm1(x) { - return Math.exp(x) - 1; -} - -function log1p(x) { - return Math.log(1 + x); -} - -function log2(x) { - return Math.log(x) / Math.LN2; -} - -class Parser { - constructor(options) { - this.options = options || {}; - this.unaryOps = { - sin: Math.sin, - cos: Math.cos, - tan: Math.tan, - asin: Math.asin, - acos: Math.acos, - atan: Math.atan, - sinh: Math.sinh || sinh, - cosh: Math.cosh || cosh, - tanh: Math.tanh || tanh, - asinh: Math.asinh || asinh, - acosh: Math.acosh || acosh, - atanh: Math.atanh || atanh, - sqrt: Math.sqrt, - cbrt: Math.cbrt || cbrt, - log: Math.log, - log2: Math.log2 || log2, - ln: Math.log, - lg: Math.log10 || log10, - log10: Math.log10 || log10, - expm1: Math.expm1 || expm1, - log1p: Math.log1p || log1p, - abs: Math.abs, - ceil: Math.ceil, - floor: Math.floor, - round: Math.round, - trunc: Math.trunc || trunc, - '-': neg, - '+': Number, - exp: Math.exp, - not: not, - length: stringOrArrayLength, - '!': factorial, - sign: Math.sign || sign - }; - - this.binaryOps = { - '+': add, - '-': sub, - '*': mul, - '/': div, - '%': mod, - '^': Math.pow, - '||': concat, - '==': equal, - '!=': notEqual, - '>': greaterThan, - '<': lessThan, - '>=': greaterThanEqual, - '<=': lessThanEqual, - and: andOperator, - or: orOperator, - 'in': inOperator, - '=': setVar, - '[': arrayIndex - }; - - this.ternaryOps = { - '?': condition - }; - - this.functions = { - random: random, - fac: factorial, - min: min, - max: max, - hypot: Math.hypot || hypot, - pyt: Math.hypot || hypot, // backward compat - pow: Math.pow, - atan2: Math.atan2, - 'if': condition, - gamma: gamma, - 'Γ': gamma, - roundTo: roundTo, - map: arrayMap, - fold: arrayFold, - filter: arrayFilter, - indexOf: stringOrArrayIndexOf, - join: arrayJoin - }; - - // These constants will automatically be replaced the MOMENT they are parsed. - // (Original consts from the parser) - this.builtinConsts = {}; - // These consts will only be replaced when the expression is evaluated. - this.consts = {} - - } - - parse(expr) { - var instr = []; - var parserState = new ParserState( - this, - new TokenStream(this, expr), - { allowMemberAccess: this.options.allowMemberAccess } - ); - - parserState.parseExpression(instr); - parserState.expect(TEOF, QT_TRANSLATE_NOOP('error','EOF')); - - return new Expression(instr, this); - } - - evaluate(expr, variables) { - return this.parse(expr).evaluate(variables); - } -}; - - -var sharedParser = new Parser(); - -Parser.parse = function (expr) { - return sharedParser.parse(expr); -}; - -Parser.evaluate = function (expr, variables) { - return sharedParser.parse(expr).evaluate(variables); -}; - -var optionNameMap = { - '+': 'add', - '-': 'subtract', - '*': 'multiply', - '/': 'divide', - '%': 'remainder', - '^': 'power', - '!': 'factorial', - '<': 'comparison', - '>': 'comparison', - '<=': 'comparison', - '>=': 'comparison', - '==': 'comparison', - '!=': 'comparison', - '||': 'concatenate', - 'and': 'logical', - 'or': 'logical', - 'not': 'logical', - '?': 'conditional', - ':': 'conditional', - //'=': 'assignment', // Disable assignment - '[': 'array', - //'()=': 'fndef' // Diable function definition -}; - -function getOptionName(op) { - return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op; -} - -Parser.prototype.isOperatorEnabled = function (op) { - var optionName = getOptionName(op); - var operators = this.options.operators || {}; - - return !(optionName in operators) || !!operators[optionName]; -}; - -/*! - Based on ndef.parser, by Raphael Graf - http://www.undefined.ch/mparser/index.html - - Ported to JavaScript and modified by Matthew Crumley (http://silentmatt.com/) - - Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) - - You are free to use and modify this code in anyway you find useful. Please leave this comment in the code - to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, - but don't feel like you have to let me know or ask permission. -*/ - - diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expression.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expression.mjs new file mode 100644 index 0000000..d7aba35 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/expression.mjs @@ -0,0 +1,540 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +import { + Instruction, + IOP3, IOP2, IOP1, + INUMBER, IARRAY, + IVAR, IVARNAME, + IEXPR, IEXPREVAL, + IMEMBER, IFUNCALL, + IENDSTATEMENT, + unaryInstruction, binaryInstruction, ternaryInstruction +} from "./instruction.mjs" + +/** + * Simplifies the given instructions + * @param {Instruction[]} tokens + * @param {Record.} unaryOps + * @param {Record.} binaryOps + * @param {Record.} ternaryOps + * @param {Record.} values + * @return {Instruction[]} + */ +function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) { + const nstack = [] + const newexpression = [] + let n1, n2, n3 + let f + for(let i = 0; i < tokens.length; i++) { + let item = tokens[i] + const type = item.type + if(type === INUMBER || type === IVARNAME) { + if(Array.isArray(item.value)) { + nstack.push.apply(nstack, simplify(item.value.map(function(x) { + return new Instruction(INUMBER, x) + }).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values)) + } else { + nstack.push(item) + } + } else if(type === IVAR && values.hasOwnProperty(item.value)) { + item = new Instruction(INUMBER, values[item.value]) + nstack.push(item) + } else if(type === IOP2 && nstack.length > 1) { + n2 = nstack.pop() + n1 = nstack.pop() + f = binaryOps[item.value] + item = new Instruction(INUMBER, f(n1.value, n2.value)) + nstack.push(item) + } else if(type === IOP3 && nstack.length > 2) { + n3 = nstack.pop() + n2 = nstack.pop() + n1 = nstack.pop() + if(item.value === "?") { + nstack.push(n1.value ? n2.value : n3.value) + } else { + f = ternaryOps[item.value] + item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value)) + nstack.push(item) + } + } else if(type === IOP1 && nstack.length > 0) { + n1 = nstack.pop() + f = unaryOps[item.value] + item = new Instruction(INUMBER, f(n1.value)) + nstack.push(item) + } else if(type === IEXPR) { + while(nstack.length > 0) { + newexpression.push(nstack.shift()) + } + newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values))) + } else if(type === IMEMBER && nstack.length > 0) { + n1 = nstack.pop() + if(item.value in n1.value) + nstack.push(new Instruction(INUMBER, n1.value[item.value])) + else + throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1)) + } else { + while(nstack.length > 0) { + newexpression.push(nstack.shift()) + } + newexpression.push(item) + } + } + while(nstack.length > 0) { + newexpression.push(nstack.shift()) + } + return newexpression +} + +/** + * In the given instructions, replaces variable by expr. + * @param {Instruction[]} tokens + * @param {string} variable + * @param {number} expr + * @return {Instruction[]} + */ +function substitute(tokens, variable, expr) { + const newexpression = [] + for(let i = 0; i < tokens.length; i++) { + let item = tokens[i] + const type = item.type + if(type === IVAR && item.value === variable) { + for(let j = 0; j < expr.tokens.length; j++) { + const expritem = expr.tokens[j] + let replitem + if(expritem.type === IOP1) { + replitem = unaryInstruction(expritem.value) + } else if(expritem.type === IOP2) { + replitem = binaryInstruction(expritem.value) + } else if(expritem.type === IOP3) { + replitem = ternaryInstruction(expritem.value) + } else { + replitem = new Instruction(expritem.type, expritem.value) + } + newexpression.push(replitem) + } + } else if(type === IEXPR) { + newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr))) + } else { + newexpression.push(item) + } + } + return newexpression +} + +/** + * Evaluates the given instructions for a given Expression with given values. + * @param {Instruction[]} tokens + * @param {Expression} expr + * @param {Record.} values + * @return {number} + */ +function evaluate(tokens, expr, values) { + const nstack = [] + let n1, n2, n3 + let f, args, argCount + + if(isExpressionEvaluator(tokens)) { + return resolveExpression(tokens, values) + } + + for(let i = 0; i < tokens.length; i++) { + const item = tokens[i] + const type = item.type + if(type === INUMBER || type === IVARNAME) { + nstack.push(item.value) + } else if(type === IOP2) { + n2 = nstack.pop() + n1 = nstack.pop() + if(item.value === "and") { + nstack.push(n1 ? !!evaluate(n2, expr, values) : false) + } else if(item.value === "or") { + nstack.push(n1 ? true : !!evaluate(n2, expr, values)) + } else if(item.value === "=") { + f = expr.binaryOps[item.value] + nstack.push(f(n1, evaluate(n2, expr, values), values)) + } else { + f = expr.binaryOps[item.value] + nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))) + } + } else if(type === IOP3) { + n3 = nstack.pop() + n2 = nstack.pop() + n1 = nstack.pop() + if(item.value === "?") { + nstack.push(evaluate(n1 ? n2 : n3, expr, values)) + } else { + f = expr.ternaryOps[item.value] + nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values))) + } + } else if(type === IVAR) { + // Check for variable value + if(/^__proto__|prototype|constructor$/.test(item.value)) { + throw new Error("WARNING: Prototype access detected and denied. If you downloaded this file from the internet, this file might be a virus.") + } else if(item.value in expr.functions) { + nstack.push(expr.functions[item.value]) + } else if(item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) { + nstack.push(expr.unaryOps[item.value]) + } else { + const v = values[item.value] + if(v !== undefined) { + nstack.push(v) + } else { + throw new Error(qsTranslate("error", "Undefined variable %1.").arg(item.value)) + } + } + } else if(type === IOP1) { + n1 = nstack.pop() + f = expr.unaryOps[item.value] + nstack.push(f(resolveExpression(n1, values))) + } else if(type === IFUNCALL) { + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(resolveExpression(nstack.pop(), values)) + } + f = nstack.pop() + if(f.apply && f.call) { + nstack.push(f.apply(undefined, args)) + } else if(f.execute) { + // Objects & expressions execution + if(args.length >= 1) + nstack.push(f.execute.apply(f, args)) + else + throw new Error(qsTranslate("error", "In order to be executed, object %1 must have at least one argument.").arg(f)) + } else { + throw new Error(qsTranslate("error", "%1 cannot be executed.").arg(f)) + } + } else if(type === IEXPR) { + nstack.push(createExpressionEvaluator(item, expr)) + } else if(type === IEXPREVAL) { + nstack.push(item) + } else if(type === IMEMBER) { + n1 = nstack.pop() + if(item.value in n1) + if(n1[item.value].execute && n1[item.value].cached) + nstack.push(n1[item.value].execute()) + else + nstack.push(n1[item.value]) + else + throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1)) + } else if(type === IENDSTATEMENT) { + nstack.pop() + } else if(type === IARRAY) { + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(nstack.pop()) + } + nstack.push(args) + } else { + throw new Error(qsTranslate("error", "Invalid expression.")) + } + } + if(nstack.length > 1) { + throw new Error(qsTranslate("error", "Invalid expression (parity).")) + } + // Explicitly return zero to avoid test issues caused by -0 + return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values) +} + +function createExpressionEvaluator(token, expr) { + if(isExpressionEvaluator(token)) return token + return { + type: IEXPREVAL, + value: function(scope) { + return evaluate(token.value, expr, scope) + } + } +} + +function isExpressionEvaluator(n) { + return n && n.type === IEXPREVAL +} + +function resolveExpression(n, values) { + return isExpressionEvaluator(n) ? n.value(values) : n +} + +/** + * Converts the given instructions to a string + * If toJS is active, can be evaluated with eval, otherwise it can be reparsed by the parser. + * @param {Instruction[]} tokens + * @param {boolean} toJS + * @return {string} + */ +function expressionToString(tokens, toJS) { + let nstack = [] + let n1, n2, n3 + let f, args, argCount + for(let i = 0; i < tokens.length; i++) { + const item = tokens[i] + const type = item.type + if(type === INUMBER) { + if(typeof item.value === "number" && item.value < 0) { + nstack.push("(" + item.value + ")") + } else if(Array.isArray(item.value)) { + nstack.push("[" + item.value.map(escapeValue).join(", ") + "]") + } else { + nstack.push(escapeValue(item.value)) + } + } else if(type === IOP2) { + n2 = nstack.pop() + n1 = nstack.pop() + f = item.value + if(toJS) { + if(f === "^") { + nstack.push("Math.pow(" + n1 + ", " + n2 + ")") + } else if(f === "and") { + nstack.push("(!!" + n1 + " && !!" + n2 + ")") + } else if(f === "or") { + nstack.push("(!!" + n1 + " || !!" + n2 + ")") + } else if(f === "||") { + nstack.push("(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((" + n1 + "),(" + n2 + ")))") + } else if(f === "==") { + nstack.push("(" + n1 + " === " + n2 + ")") + } else if(f === "!=") { + nstack.push("(" + n1 + " !== " + n2 + ")") + } else if(f === "[") { + nstack.push(n1 + "[(" + n2 + ") | 0]") + } else { + nstack.push("(" + n1 + " " + f + " " + n2 + ")") + } + } else { + if(f === "[") { + nstack.push(n1 + "[" + n2 + "]") + } else { + nstack.push("(" + n1 + " " + f + " " + n2 + ")") + } + } + } else if(type === IOP3) { + n3 = nstack.pop() + n2 = nstack.pop() + n1 = nstack.pop() + f = item.value + if(f === "?") { + nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")") + } else { + throw new Error(qsTranslate("error", "Invalid expression.")) + } + } else if(type === IVAR || type === IVARNAME) { + nstack.push(item.value) + } else if(type === IOP1) { + n1 = nstack.pop() + f = item.value + if(f === "-" || f === "+") { + nstack.push("(" + f + n1 + ")") + } else if(toJS) { + if(f === "not") { + nstack.push("(" + "!" + n1 + ")") + } else if(f === "!") { + nstack.push("fac(" + n1 + ")") + } else { + nstack.push(f + "(" + n1 + ")") + } + } else if(f === "!") { + nstack.push("(" + n1 + "!)") + } else { + nstack.push("(" + f + " " + n1 + ")") + } + } else if(type === IFUNCALL) { + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(nstack.pop()) + } + f = nstack.pop() + nstack.push(f + "(" + args.join(", ") + ")") + } else if(type === IMEMBER) { + n1 = nstack.pop() + nstack.push(n1 + "." + item.value) + } else if(type === IARRAY) { + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(nstack.pop()) + } + nstack.push("[" + args.join(", ") + "]") + } else if(type === IEXPR) { + nstack.push("(" + expressionToString(item.value, toJS) + ")") + } else if(type === IENDSTATEMENT) { + + } else { + throw new Error(qsTranslate("error", "Invalid expression.")) + } + } + if(nstack.length > 1) { + if(toJS) { + nstack = [nstack.join(",")] + } else { + nstack = [nstack.join(";")] + } + } + return String(nstack[0]) +} + +export function escapeValue(v) { + if(typeof v === "string") { + return JSON.stringify(v).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029") + } + return v +} + +/** + * Pushes all symbols from tokens into the symbols array. + * @param {Instruction[]} tokens + * @param {string[]} symbols + * @param {{withMembers: (boolean|undefined)}}options + */ +function getSymbols(tokens, symbols, options) { + options = options || {} + const withMembers = !!options.withMembers + let prevVar = null + + for(let i = 0; i < tokens.length; i++) { + const item = tokens[i] + if(item.type === IVAR || item.type === IVARNAME) { + if(!withMembers && !symbols.includes(item.value)) { + symbols.push(item.value) + } else if(prevVar !== null) { + if(!symbols.includes(prevVar)) { + symbols.push(prevVar) + } + prevVar = item.value + } else { + prevVar = item.value + } + } else if(item.type === IMEMBER && withMembers && prevVar !== null) { + prevVar += "." + item.value + } else if(item.type === IEXPR) { + getSymbols(item.value, symbols, options) + } else if(prevVar !== null) { + if(!symbols.includes(prevVar)) { + symbols.push(prevVar) + } + prevVar = null + } + } + + if(prevVar !== null && !symbols.includes(prevVar)) { + symbols.push(prevVar) + } +} + +export class Expression { + /** + * @param {Instruction[]} tokens + * @param {Parser} parser + */ + constructor(tokens, parser) { + this.tokens = tokens + this.parser = parser + this.unaryOps = parser.unaryOps + this.binaryOps = parser.binaryOps + this.ternaryOps = parser.ternaryOps + this.functions = parser.functions + } + + /** + * Simplifies the expression. + * @param {Object|undefined} values + * @returns {Expression} + */ + simplify(values) { + values = values || {} + return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser) + } + + /** + * Creates a new expression where the variable is substituted by the given expression. + * @param {string} variable + * @param {string|Expression} expr + * @returns {Expression} + */ + substitute(variable, expr) { + if(!(expr instanceof Expression)) { + expr = this.parser.parse(String(expr)) + } + + return new Expression(substitute(this.tokens, variable, expr), this.parser) + } + + /** + * Calculates the value of the expression by giving all variables and their corresponding values. + * @param {Object} values + * @returns {number} + */ + evaluate(values) { + values = Object.assign({}, values, this.parser.consts) + return evaluate(this.tokens, this, values) + } + + /** + * Returns a list of symbols (string of characters) in the expressions. + * Can be functions, constants, or variables. + * @returns {string[]} + */ + symbols(options) { + options = options || {} + const vars = [] + getSymbols(this.tokens, vars, options) + return vars + } + + toString() { + return expressionToString(this.tokens, false) + } + + + /** + * Returns the list of symbols (string of characters) which are not defined + * as constants or functions. + * @returns {string[]} + */ + variables(options) { + options = options || {} + const vars = [] + getSymbols(this.tokens, vars, options) + const functions = this.functions + const consts = this.parser.consts + return vars.filter((name) => { + return !(name in functions) && !(name in consts) + }) + } + + + /** + * Converts the expression to a JS function. + * @param {string} param - Parsed variables for the function. + * @param {Object.} variables - Default variables to provide. + * @returns {function(...any)} + */ + toJSFunction(param, variables) { + const expr = this + const f = new Function(param, "with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return " + expressionToString(this.simplify(variables).tokens, true) + "; }") // eslint-disable-line no-new-func + return function() { + return f.apply(expr, arguments) + } + } +} \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/instruction.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/instruction.mjs new file mode 100644 index 0000000..df0e2d7 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/instruction.mjs @@ -0,0 +1,82 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +export const INUMBER = "INUMBER" +export const IOP1 = "IOP1" +export const IOP2 = "IOP2" +export const IOP3 = "IOP3" +export const IVAR = "IVAR" +export const IVARNAME = "IVARNAME" +export const IFUNCALL = "IFUNCALL" +export const IEXPR = "IEXPR" +export const IEXPREVAL = "IEXPREVAL" +export const IMEMBER = "IMEMBER" +export const IENDSTATEMENT = "IENDSTATEMENT" +export const IARRAY = "IARRAY" + + +export class Instruction { + /** + * + * @param {string} type + * @param {any} value + */ + constructor(type, value) { + this.type = type + this.value = (value !== undefined && value !== null) ? value : 0 + } + + toString() { + switch(this.type) { + case INUMBER: + case IOP1: + case IOP2: + case IOP3: + case IVAR: + case IVARNAME: + case IENDSTATEMENT: + return this.value + case IFUNCALL: + return "CALL " + this.value + case IARRAY: + return "ARRAY " + this.value + case IMEMBER: + return "." + this.value + default: + return "Invalid Instruction" + } + } +} + +export function unaryInstruction(value) { + return new Instruction(IOP1, value) +} + +export function binaryInstruction(value) { + return new Instruction(IOP2, value) +} + +export function ternaryInstruction(value) { + return new Instruction(IOP3, value) +} \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.mjs similarity index 68% rename from LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.js rename to LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.mjs index 998ed32..244194f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/integration.mjs @@ -1,25 +1,23 @@ /** * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. * Copyright (C) 2021-2024 Ad5001 - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -.pragma library - -.import "expr-eval.js" as ExprEval -.import "../../modules.mjs" as M +import { Module } from "../../modules.mjs" +import { Parser } from "./parser.mjs" const evalVariables = { // Variables not provided by expr-eval.js, needs to be provided manually @@ -36,15 +34,14 @@ const evalVariables = { "false": false } -class ExprParserAPI extends M.Module { +export class ExprParserAPI extends Module { constructor() { - super('ExprParser', [ + super("ExprParser", [ /** @type {ObjectsAPI} */ Modules.Objects ]) this.currentVars = {} - this.Internals = ExprEval - this._parser = new ExprEval.Parser() + this._parser = new Parser() this._parser.consts = Object.assign({}, this._parser.consts, evalVariables) @@ -65,18 +62,18 @@ class ExprParserAPI extends M.Module { if(args.length === 1) { // Parse object f = args[0] - if(typeof f !== 'object' || !f.execute) - throw EvalError(qsTranslate('usage', 'Usage: %1').arg(usage1)) + if(typeof f !== "object" || !f.execute) + throw EvalError(qsTranslate("usage", "Usage: %1").arg(usage1)) let target = f f = (x) => target.execute(x) } else if(args.length === 2) { // Parse variable - [f,variable] = args - if(typeof f !== 'string' || typeof variable !== 'string') - throw EvalError(qsTranslate('usage', 'Usage: %1').arg(usage2)) + [f, variable] = args + if(typeof f !== "string" || typeof variable !== "string") + throw EvalError(qsTranslate("usage", "Usage: %1").arg(usage2)) f = this._parser.parse(f).toJSFunction(variable, this.currentVars) } else - throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2)) + throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2)) return f } @@ -88,27 +85,27 @@ class ExprParserAPI extends M.Module { } integral(a, b, ...args) { - let usage1 = qsTranslate('usage', 'integral(, , )') - let usage2 = qsTranslate('usage', 'integral(, , , )') + let usage1 = qsTranslate("usage", "integral(, , )") + let usage2 = qsTranslate("usage", "integral(, , , )") let f = this.parseArgumentsForFunction(args, usage1, usage2) if(a == null || b == null) - throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2)) + throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2)) // https://en.wikipedia.org/wiki/Simpson%27s_rule // Simpler, faster than tokenizing the expression - return (b-a)/6*(f(a)+4*f((a+b)/2)+f(b)) + return (b - a) / 6 * (f(a) + 4 * f((a + b) / 2) + f(b)) } derivative(...args) { - let usage1 = qsTranslate('usage', 'derivative(, )') - let usage2 = qsTranslate('usage', 'derivative(, , )') + let usage1 = qsTranslate("usage", "derivative(, )") + let usage2 = qsTranslate("usage", "derivative(, , )") let x = args.pop() let f = this.parseArgumentsForFunction(args, usage1, usage2) if(x == null) - throw EvalError(qsTranslate('usage', 'Usage: %1 or\n%2').arg(usage1).arg(usage2)) + throw EvalError(qsTranslate("usage", "Usage: %1 or\n%2").arg(usage1).arg(usage2)) - let derivative_precision = x/10 - return (f(x+derivative_precision/2)-f(x-derivative_precision/2))/derivative_precision + let derivative_precision = x / 10 + return (f(x + derivative_precision / 2) - f(x - derivative_precision / 2)) / derivative_precision } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parser.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parser.mjs new file mode 100644 index 0000000..3336fcf --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parser.mjs @@ -0,0 +1,172 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +import * as Polyfill from "./polyfill.mjs" +import { ParserState } from "./parserstate.mjs" +import { TEOF, TokenStream } from "./tokens.mjs" +import { Expression } from "./expression.mjs" + +const optionNameMap = { + "+": "add", + "-": "subtract", + "*": "multiply", + "/": "divide", + "%": "remainder", + "^": "power", + "!": "factorial", + "<": "comparison", + ">": "comparison", + "<=": "comparison", + ">=": "comparison", + "==": "comparison", + "!=": "comparison", + "||": "concatenate", + "and": "logical", + "or": "logical", + "not": "logical", + "?": "conditional", + ":": "conditional", + //'=': 'assignment', // Disable assignment + "[": "array" + //'()=': 'fndef' // Diable function definition +} + +export class Parser { + constructor(options) { + this.options = options || {} + this.unaryOps = { + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + asin: Math.asin, + acos: Math.acos, + atan: Math.atan, + sinh: Math.sinh || Polyfill.sinh, + cosh: Math.cosh || Polyfill.cosh, + tanh: Math.tanh || Polyfill.tanh, + asinh: Math.asinh || Polyfill.asinh, + acosh: Math.acosh || Polyfill.acosh, + atanh: Math.atanh || Polyfill.atanh, + sqrt: Math.sqrt, + cbrt: Math.cbrt || Polyfill.cbrt, + log: Math.log, + log2: Math.log2 || Polyfill.log2, + ln: Math.log, + lg: Math.log10 || Polyfill.log10, + log10: Math.log10 || Polyfill.log10, + expm1: Math.expm1 || Polyfill.expm1, + log1p: Math.log1p || Polyfill.log1p, + abs: Math.abs, + ceil: Math.ceil, + floor: Math.floor, + round: Math.round, + trunc: Math.trunc || Polyfill.trunc, + "-": Polyfill.neg, + "+": Number, + exp: Math.exp, + not: Polyfill.not, + length: Polyfill.stringOrArrayLength, + "!": Polyfill.factorial, + sign: Math.sign || Polyfill.sign + } + this.unaryOpsList = Object.keys(this.unaryOps) + + this.binaryOps = { + "+": Polyfill.add, + "-": Polyfill.sub, + "*": Polyfill.mul, + "/": Polyfill.div, + "%": Polyfill.mod, + "^": Math.pow, + "||": Polyfill.concat, + "==": Polyfill.equal, + "!=": Polyfill.notEqual, + ">": Polyfill.greaterThan, + "<": Polyfill.lessThan, + ">=": Polyfill.greaterThanEqual, + "<=": Polyfill.lessThanEqual, + and: Polyfill.andOperator, + or: Polyfill.orOperator, + "in": Polyfill.inOperator, + "=": Polyfill.setVar, + "[": Polyfill.arrayIndex + } + + this.ternaryOps = { + "?": Polyfill.condition + } + + this.functions = { + random: Polyfill.random, + fac: Polyfill.factorial, + min: Polyfill.min, + max: Polyfill.max, + hypot: Math.hypot || Polyfill.hypot, + pyt: Math.hypot || Polyfill.hypot, // backward compat + pow: Math.pow, + atan2: Math.atan2, + "if": Polyfill.condition, + gamma: Polyfill.gamma, + "Γ": Polyfill.gamma, + roundTo: Polyfill.roundTo, + map: Polyfill.arrayMap, + fold: Polyfill.arrayFold, + filter: Polyfill.arrayFilter, + indexOf: Polyfill.stringOrArrayIndexOf, + join: Polyfill.arrayJoin + } + + // These constants will automatically be replaced the MOMENT they are parsed. + // (Original consts from the parser) + this.builtinConsts = {} + // These consts will only be replaced when the expression is evaluated. + this.consts = {} + + } + + parse(expr) { + const instr = [] + const parserState = new ParserState( + this, + new TokenStream(this, expr), + { allowMemberAccess: this.options.allowMemberAccess } + ) + + parserState.parseExpression(instr) + parserState.expect(TEOF, QT_TRANSLATE_NOOP("error", "EOF")) + + return new Expression(instr, this) + } + + evaluate(expr, variables) { + return this.parse(expr).evaluate(variables) + } + + isOperatorEnabled(op) { + const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op + const operators = this.options.operators || {} + + return !(optionName in operators) || !!operators[optionName] + } +} \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parserstate.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parserstate.mjs new file mode 100644 index 0000000..801c424 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/parserstate.mjs @@ -0,0 +1,398 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +import { TBRACKET, TCOMMA, TEOF, TNAME, TNUMBER, TOP, TPAREN, TSTRING } from "./tokens.mjs" +import { + Instruction, + IARRAY, IEXPR, IFUNCALL, IMEMBER, + INUMBER, IVAR, + ternaryInstruction, binaryInstruction, unaryInstruction +} from "./instruction.mjs" + +const COMPARISON_OPERATORS = ["==", "!=", "<", "<=", ">=", ">", "in"] +const ADD_SUB_OPERATORS = ["+", "-", "||"] +const TERM_OPERATORS = ["*", "/", "%"] + +export class ParserState { + /** + * + * @param {Parser} parser + * @param {TokenStream} tokenStream + * @param {{[operators]: Object., [allowMemberAccess]: boolean}} options + */ + constructor(parser, tokenStream, options) { + this.parser = parser + this.tokens = tokenStream + this.current = null + this.nextToken = null + this.next() + this.savedCurrent = null + this.savedNextToken = null + this.allowMemberAccess = options.allowMemberAccess !== false + } + + /** + * Queries the next token for parsing. + * @return {Token} + */ + next() { + this.current = this.nextToken + this.nextToken = this.tokens.next() + return this.nextToken + } + + /** + * Checks if a given Token matches a condition (called if function, one of if array, and exact match otherwise) + * @param {Token} token + * @param {Array|function(Token): boolean|string|number|boolean} [value] + * @return {boolean} + */ + tokenMatches(token, value) { + if(typeof value === "undefined") { + return true + } else if(Array.isArray(value)) { + return value.includes(token.value) + } else if(typeof value === "function") { + return value(token) + } else { + return token.value === value + } + } + + /** + * Saves the current state (current and next token) to be restored later. + */ + save() { + this.savedCurrent = this.current + this.savedNextToken = this.nextToken + this.tokens.save() + } + + /** + * Restores a previous state (current and next token) from last save. + */ + restore() { + this.tokens.restore() + this.current = this.savedCurrent + this.nextToken = this.savedNextToken + } + + /** + * Checks if the next token matches the given type and value, and if so, consume the current token. + * Returns true if the check matches. + * @param {string} type + * @param {any} [value] + * @return {boolean} + */ + accept(type, value) { + if(this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) { + this.next() + return true + } + return false + } + + /** + * Throws an error if the next token does not match the given type and value. Otherwise, consumes the current token. + * @param {string} type + * @param {any} [value] + */ + expect(type, value) { + if(!this.accept(type, value)) { + throw new Error(qsTranslate("error", "Parse error [position %1]: %2") + .arg(this.tokens.pos) + .arg(qsTranslate("error", "Expected %1").arg(value || type))) + } + } + + /** + * Converts enough Tokens to form an expression atom (generally the next part of the expression) into an instruction + * and pushes it to the instruction list. + * Throws an error if an unexpected token gets parsed. + * @param {Instruction[]} instr + */ + parseAtom(instr) { + const prefixOperators = this.tokens.unaryOpsList + + if(this.accept(TNAME) || this.accept(TOP, prefixOperators)) { + instr.push(new Instruction(IVAR, this.current.value)) + } else if(this.accept(TNUMBER)) { + instr.push(new Instruction(INUMBER, this.current.value)) + } else if(this.accept(TSTRING)) { + instr.push(new Instruction(INUMBER, this.current.value)) + } else if(this.accept(TPAREN, "(")) { + this.parseExpression(instr) + this.expect(TPAREN, ")") + } else if(this.accept(TBRACKET, "[")) { + if(this.accept(TBRACKET, "]")) { + instr.push(new Instruction(IARRAY, 0)) + } else { + const argCount = this.parseArrayList(instr) + instr.push(new Instruction(IARRAY, argCount)) + } + } else { + throw new Error(qsTranslate("error", "Unexpected %1").arg(this.nextToken)) + } + } + + /** + * Consumes the next tokens to compile a general expression which should return a value, and compiles + * the instructions into the list. + * @param {Instruction[]} instr + */ + parseExpression(instr) { + const exprInstr = [] + this.parseConditionalExpression(exprInstr) + instr.push(...exprInstr) + } + + /** + * Parses an array indice, and return the number of arguments found at the end. + * @param {Instruction[]} instr + * @return {number} + */ + parseArrayList(instr) { + let argCount = 0 + + while(!this.accept(TBRACKET, "]")) { + this.parseExpression(instr) + ++argCount + while(this.accept(TCOMMA)) { + this.parseExpression(instr) + ++argCount + } + } + + return argCount + } + + /** + * Parses a tertiary statement ( ? : ) and pushes it into the instruction + * list. + * @param {Instruction[]} instr + */ + parseConditionalExpression(instr) { + this.parseOrExpression(instr) + while(this.accept(TOP, "?")) { + const trueBranch = [] + const falseBranch = [] + this.parseConditionalExpression(trueBranch) + this.expect(TOP, ":") + this.parseConditionalExpression(falseBranch) + instr.push(new Instruction(IEXPR, trueBranch)) + instr.push(new Instruction(IEXPR, falseBranch)) + instr.push(ternaryInstruction("?")) + } + } + + /** + * Parses a binary or statement ( or ) and pushes it into the instruction list. + * @param {Instruction[]} instr + */ + parseOrExpression(instr) { + this.parseAndExpression(instr) + while(this.accept(TOP, "or")) { + const falseBranch = [] + this.parseAndExpression(falseBranch) + instr.push(new Instruction(IEXPR, falseBranch)) + instr.push(binaryInstruction("or")) + } + } + + /** + * Parses a binary and statement ( and ) and pushes it into the instruction list. + * @param {Instruction[]} instr + */ + parseAndExpression(instr) { + this.parseComparison(instr) + while(this.accept(TOP, "and")) { + const trueBranch = [] + this.parseComparison(trueBranch) + instr.push(new Instruction(IEXPR, trueBranch)) + instr.push(binaryInstruction("and")) + } + } + + /** + * Parses a binary equality statement ( == and so on) and pushes it into the instruction list. + * @param {Instruction[]} instr + */ + parseComparison(instr) { + this.parseAddSub(instr) + while(this.accept(TOP, COMPARISON_OPERATORS)) { + const op = this.current + this.parseAddSub(instr) + instr.push(binaryInstruction(op.value)) + } + } + + /** + * Parses add, minus and concat operations and pushes them into the instruction list. + * @param {Instruction[]} instr + */ + parseAddSub(instr) { + this.parseTerm(instr) + while(this.accept(TOP, ADD_SUB_OPERATORS)) { + const op = this.current + this.parseTerm(instr) + instr.push(binaryInstruction(op.value)) + } + } + + /** + * Parses times, divide and modulo operations and pushes them into the instruction list. + * @param {Instruction[]} instr + */ + parseTerm(instr) { + this.parseFactor(instr) + while(this.accept(TOP, TERM_OPERATORS)) { + const op = this.current + this.parseFactor(instr) + instr.push(binaryInstruction(op.value)) + } + } + + /** + * Parses prefix operations (+, -, but also functions like sin or cos which don't need parentheses) + * @param {Instruction[]} instr + */ + parseFactor(instr) { + const prefixOperators = this.tokens.unaryOpsList + + this.save() + if(this.accept(TOP, prefixOperators)) { + if(this.current.value !== "-" && this.current.value !== "+") { + if(this.nextToken.type === TPAREN && this.nextToken.value === "(") { + this.restore() + this.parseExponential(instr) + return + } else if(this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ")")) { + this.restore() + this.parseAtom(instr) + return + } + } + + const op = this.current + this.parseFactor(instr) + instr.push(unaryInstruction(op.value)) + } else { + this.parseExponential(instr) + } + } + + /** + * + * @param {Instruction[]} instr + */ + parseExponential(instr) { + this.parsePostfixExpression(instr) + while(this.accept(TOP, "^")) { + this.parseFactor(instr) + instr.push(binaryInstruction("^")) + } + } + + + /** + * Parses factorial '!' (after the expression to apply it to). + * @param {Instruction[]} instr + */ + parsePostfixExpression(instr) { + this.parseFunctionCall(instr) + while(this.accept(TOP, "!")) { + instr.push(unaryInstruction("!")) + } + } + + /** + * Parse a function (name + parentheses + arguments). + * @param {Instruction[]} instr + */ + parseFunctionCall(instr) { + const prefixOperators = this.tokens.unaryOpsList + + if(this.accept(TOP, prefixOperators)) { + const op = this.current + this.parseAtom(instr) + instr.push(unaryInstruction(op.value)) + } else { + this.parseMemberExpression(instr) + while(this.accept(TPAREN, "(")) { + if(this.accept(TPAREN, ")")) { + instr.push(new Instruction(IFUNCALL, 0)) + } else { + const argCount = this.parseArgumentList(instr) + instr.push(new Instruction(IFUNCALL, argCount)) + } + } + } + } + + /** + * Parses a list of arguments, return their quantity. + * @param {Instruction[]} instr + * @return {number} + */ + parseArgumentList(instr) { + let argCount = 0 + + while(!this.accept(TPAREN, ")")) { + this.parseExpression(instr) + ++argCount + while(this.accept(TCOMMA)) { + this.parseExpression(instr) + ++argCount + } + } + + return argCount + } + + parseMemberExpression(instr) { + this.parseAtom(instr) + while(this.accept(TOP, ".") || this.accept(TBRACKET, "[")) { + const op = this.current + + if(op.value === ".") { + if(!this.allowMemberAccess) { + throw new Error(qsTranslate("error", "Unexpected \".\": member access is not permitted")) + } + + this.expect(TNAME) + instr.push(new Instruction(IMEMBER, this.current.value)) + } else if(op.value === "[") { + if(!this.tokens.isOperatorEnabled("[")) { + throw new Error(qsTranslate("error", "Unexpected \"[]\": arrays are disabled.")) + } + + this.parseExpression(instr) + this.expect(TBRACKET, "]") + instr.push(binaryInstruction("[")) + } else { + throw new Error(qsTranslate("error", "Unexpected symbol: %1.").arg(op.value)) + } + } + } +} diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/polyfill.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/polyfill.mjs new file mode 100644 index 0000000..9e8e885 --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/polyfill.mjs @@ -0,0 +1,371 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +export function add(a, b) { + return Number(a) + Number(b) +} + +export function sub(a, b) { + return a - b +} + +export function mul(a, b) { + return a * b +} + +export function div(a, b) { + return a / b +} + +export function mod(a, b) { + return a % b +} + +export function concat(a, b) { + if(Array.isArray(a) && Array.isArray(b)) { + return a.concat(b) + } + return "" + a + b +} + +export function equal(a, b) { + return a === b +} + +export function notEqual(a, b) { + return a !== b +} + +export function greaterThan(a, b) { + return a > b +} + +export function lessThan(a, b) { + return a < b +} + +export function greaterThanEqual(a, b) { + return a >= b +} + +export function lessThanEqual(a, b) { + return a <= b +} + +export function andOperator(a, b) { + return Boolean(a && b) +} + +export function orOperator(a, b) { + return Boolean(a || b) +} + +export function inOperator(a, b) { + return b.includes(a) +} + +export function sinh(a) { + return ((Math.exp(a) - Math.exp(-a)) / 2) +} + +export function cosh(a) { + return ((Math.exp(a) + Math.exp(-a)) / 2) +} + +export function tanh(a) { + if(a === Infinity) return 1 + if(a === -Infinity) return -1 + return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)) +} + +export function asinh(a) { + if(a === -Infinity) return a + return Math.log(a + Math.sqrt((a * a) + 1)) +} + +export function acosh(a) { + return Math.log(a + Math.sqrt((a * a) - 1)) +} + +export function atanh(a) { + return (Math.log((1 + a) / (1 - a)) / 2) +} + +export function log10(a) { + return Math.log(a) * Math.LOG10E +} + +export function neg(a) { + return -a +} + +export function not(a) { + return !a +} + +export function trunc(a) { + return a < 0 ? Math.ceil(a) : Math.floor(a) +} + +export function random(a) { + return Math.random() * (a || 1) +} + +export function factorial(a) { // a! + return gamma(a + 1) +} + +export function isInteger(value) { + return isFinite(value) && (value === Math.round(value)) +} + +const GAMMA_G = 4.7421875 +const GAMMA_P = [ + 0.99999999999999709182, + 57.156235665862923517, -59.597960355475491248, + 14.136097974741747174, -0.49191381609762019978, + 0.33994649984811888699e-4, + 0.46523628927048575665e-4, -0.98374475304879564677e-4, + 0.15808870322491248884e-3, -0.21026444172410488319e-3, + 0.21743961811521264320e-3, -0.16431810653676389022e-3, + 0.84418223983852743293e-4, -0.26190838401581408670e-4, + 0.36899182659531622704e-5 +] + +// Gamma function from math.js +export function gamma(n) { + let t, x + + if(isInteger(n)) { + if(n <= 0) { + return isFinite(n) ? Infinity : NaN + } + + if(n > 171) { + return Infinity // Will overflow + } + + let value = n - 2 + let res = n - 1 + while(value > 1) { + res *= value + value-- + } + + if(res === 0) { + res = 1 // 0! is per definition 1 + } + + return res + } + + if(n < 0.5) { + return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n)) + } + + if(n >= 171.35) { + return Infinity // will overflow + } + + if(n > 85.0) { // Extended Stirling Approx + const twoN = n * n + const threeN = twoN * n + const fourN = threeN * n + const fiveN = fourN * n + return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) * + (1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) - + (571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) + + (5246819 / (75246796800 * fiveN * n))) + } + + --n + x = GAMMA_P[0] + for(let i = 1; i < GAMMA_P.length; ++i) { + x += GAMMA_P[i] / (n + i) + } + + t = n + GAMMA_G + 0.5 + return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x +} + +export function stringOrArrayLength(s) { + if(Array.isArray(s)) { + return s.length + } + return String(s).length +} + +export function hypot() { + let sum = 0 + let larg = 0 + for(let i = 0; i < arguments.length; i++) { + const arg = Math.abs(arguments[i]) + let div + if(larg < arg) { + div = larg / arg + sum = (sum * div * div) + 1 + larg = arg + } else if(arg > 0) { + div = arg / larg + sum += div * div + } else { + sum += arg + } + } + return larg === Infinity ? Infinity : larg * Math.sqrt(sum) +} + +export function condition(cond, yep, nope) { + return cond ? yep : nope +} + +/** + * Decimal adjustment of a number. + * From @escopecz. + * + * @param {number} value - The number. + * @param {Integer} exp - The exponent (the 10 logarithm of the adjustment base). + * @return {number} - The adjusted value. + */ +export function roundTo(value, exp) { + // If the exp is undefined or zero... + if(typeof exp === "undefined" || +exp === 0) { + return Math.round(value) + } + value = +value + exp = -(+exp) + // If the value is not a number or the exp is not an integer... + if(isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { + return NaN + } + // Shift + value = value.toString().split("e") + value = Math.round(+(value[0] + "e" + (value[1] ? (+value[1] - exp) : -exp))) + // Shift back + value = value.toString().split("e") + return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp)) +} + +export function setVar(name, value, variables) { + if(variables) variables[name] = value + return value +} + +export function arrayIndex(array, index) { + return array[index | 0] +} + +export function max(array) { + if(arguments.length === 1 && Array.isArray(array)) { + return Math.max.apply(Math, array) + } else if(arguments.length >= 1) { + return Math.max.apply(Math, arguments) + } else { + throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("max")) + } +} + +export function min(array) { + if(arguments.length === 1 && Array.isArray(array)) { + return Math.min.apply(Math, array) + } else if(arguments.length >= 1) { + return Math.min.apply(Math, arguments) + } else { + throw new EvalError(qsTranslate("error", "Function %1 must have at least one argument.").arg("min")) + } +} + +export function arrayMap(f, a) { + if(typeof f !== "function") { + throw new EvalError(qsTranslate("error", "First argument to map is not a function.")) + } + if(!Array.isArray(a)) { + throw new EvalError(qsTranslate("error", "Second argument to map is not an array.")) + } + return a.map(function(x, i) { + return f(x, i) + }) +} + +export function arrayFold(f, init, a) { + if(typeof f !== "function") { + throw new EvalError(qsTranslate("error", "First argument to fold is not a function.")) + } + if(!Array.isArray(a)) { + throw new EvalError(qsTranslate("error", "Second argument to fold is not an array.")) + } + return a.reduce(function(acc, x, i) { + return f(acc, x, i) + }, init) +} + +export function arrayFilter(f, a) { + if(typeof f !== "function") { + throw new EvalError(qsTranslate("error", "First argument to filter is not a function.")) + } + if(!Array.isArray(a)) { + throw new EvalError(qsTranslate("error", "Second argument to filter is not an array.")) + } + return a.filter(function(x, i) { + return f(x, i) + }) +} + +export function stringOrArrayIndexOf(target, s) { + if(!(Array.isArray(s) || typeof s === "string")) { + throw new Error(qsTranslate("error", "Second argument to indexOf is not a string or array.")) + } + + return s.indexOf(target) +} + +export function arrayJoin(sep, a) { + if(!Array.isArray(a)) { + throw new Error(qsTranslate("error", "Second argument to join is not an array.")) + } + + return a.join(sep) +} + +export function sign(x) { + return ((x > 0) - (x < 0)) || +x +} + +const ONE_THIRD = 1 / 3 + +export function cbrt(x) { + return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD) +} + +export function expm1(x) { + return Math.exp(x) - 1 +} + +export function log1p(x) { + return Math.log(1 + x) +} + +export function log2(x) { + return Math.log(x) / Math.LN2 +} \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/tokens.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/tokens.mjs new file mode 100644 index 0000000..919433c --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/tokens.mjs @@ -0,0 +1,575 @@ +/** + * Based on ndef.parser, by Raphael Graf + * http://www.undefined.ch/mparser/index.html + * + * Ported to JavaScript and modified by Matthew Crumley + * https://silentmatt.com/javascript-expression-evaluator/ + * + * Ported to QMLJS with modifications done accordingly done by Ad5001 (https://ad5001.eu) + * + * Copyright (c) 2015 Matthew Crumley, 2021-2024 Ad5001 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + * to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + * but don't feel like you have to let me know or ask permission. + */ + +export const TEOF = "TEOF" +export const TOP = "TOP" +export const TNUMBER = "TNUMBER" +export const TSTRING = "TSTRING" +export const TPAREN = "TPAREN" +export const TBRACKET = "TBRACKET" +export const TCOMMA = "TCOMMA" +export const TNAME = "TNAME" + + +// Additional variable characters. +export const ADDITIONAL_VARCHARS = [ + "α", "β", "γ", "δ", "ε", "ζ", "η", + "π", "θ", "κ", "λ", "μ", "ξ", "ρ", + "ς", "σ", "τ", "φ", "χ", "ψ", "ω", + "Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ", + "Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ", + "ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ", + "ₜ", "¹", "²", "³", "⁴", "⁵", "⁶", + "⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃", + "₄", "₅", "₆", "₇", "₈", "₉", "₀", + "∞", "π" +] + +export class Token { + /** + * + * @param {string} type - Type of the token (see above). + * @param {any} value - Value of the token. + * @param {number} index - Index in the string of the token. + */ + constructor(type, value, index) { + this.type = type + this.value = value + this.index = index + } + + toString() { + return this.type + ": " + this.value + } +} + +const unicodeCodePointPattern = /^[0-9a-f]{4}$/i + +export class TokenStream { + /** + * + * @param {Parser} parser + * @param {string} expression + */ + constructor(parser, expression) { + this.pos = 0 + this.current = null + this.unaryOps = parser.unaryOps + this.unaryOpsList = parser.unaryOpsList + this.binaryOps = parser.binaryOps + this.ternaryOps = parser.ternaryOps + this.builtinConsts = parser.builtinConsts + this.expression = expression + this.savedPosition = 0 + this.savedCurrent = null + this.options = parser.options + this.parser = parser + } + + /** + * + * @param {string} type - Type of the token (see above). + * @param {any} value - Value of the token. + * @param {number} [pos] - Index in the string of the token. + */ + newToken(type, value, pos) { + return new Token(type, value, pos != null ? pos : this.pos) + } + + /** + * Saves the current position and token into the object. + */ + save() { + this.savedPosition = this.pos + this.savedCurrent = this.current + } + + + /** + * Restored the saved position and token into the current. + */ + restore() { + this.pos = this.savedPosition + this.current = this.savedCurrent + } + + /** + * Consumes the character at the current position and advance it + * until it makes a valid token, and returns it. + * @returns {Token} + */ + next() { + if(this.pos >= this.expression.length) { + return this.newToken(TEOF, "EOF") + } + + if(this.isWhitespace()) { + return this.next() + } else if(this.isRadixInteger() || + this.isNumber() || + this.isOperator() || + this.isString() || + this.isParen() || + this.isBracket() || + this.isComma() || + this.isNamedOp() || + this.isConst() || + this.isName()) { + return this.current + } else { + this.parseError(qsTranslate("error", "Unknown character \"%1\".").arg(this.expression.charAt(this.pos))) + } + } + + /** + * Checks if the character at the current position starts a string, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isString() { + const startPos = this.pos + const quote = this.expression.charAt(startPos) + let r = false + + if(quote === "'" || quote === "\"") { + let index = this.expression.indexOf(quote, startPos + 1) + while(index >= 0 && this.pos < this.expression.length) { + this.pos = index + 1 + if(this.expression.charAt(index - 1) !== "\\") { + const rawString = this.expression.substring(startPos + 1, index) + this.current = this.newToken(TSTRING, this.unescape(rawString), startPos) + r = true + break + } + index = this.expression.indexOf(quote, index + 1) + } + } + return r + } + + /** + * Checks if the character at the current pos is a parenthesis, and if so consumes it into current + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isParen() { + const c = this.expression.charAt(this.pos) + if(c === "(" || c === ")") { + this.current = this.newToken(TPAREN, c) + this.pos++ + return true + } + return false + } + + /** + * Checks if the character at the current pos is a bracket, and if so consumes it into current + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isBracket() { + const c = this.expression.charAt(this.pos) + if((c === "[" || c === "]") && this.isOperatorEnabled("[")) { + this.current = this.newToken(TBRACKET, c) + this.pos++ + return true + } + return false + } + + /** + * Checks if the character at the current pos is a comma, and if so consumes it into current + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isComma() { + const c = this.expression.charAt(this.pos) + if(c === ",") { + this.current = this.newToken(TCOMMA, ",") + this.pos++ + return true + } + return false + } + + /** + * Checks if the current character is an identifier and makes a const, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isConst() { + const startPos = this.pos + let i = startPos + for(; i < this.expression.length; i++) { + const c = this.expression.charAt(i) + if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) { + if(i === this.pos || (c !== "_" && c !== "." && (c < "0" || c > "9"))) { + break + } + } + } + if(i > startPos) { + const str = this.expression.substring(startPos, i) + if(str in this.builtinConsts) { + this.current = this.newToken(TNUMBER, this.builtinConsts[str]) + this.pos += str.length + return true + } + } + return false + } + + /** + * Checks if the current character is an identifier and makes a function or an operator, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isNamedOp() { + const startPos = this.pos + let i = startPos + for(; i < this.expression.length; i++) { + const c = this.expression.charAt(i) + if(c.toUpperCase() === c.toLowerCase()) { + if(i === this.pos || (c !== "_" && (c < "0" || c > "9"))) { + break + } + } + } + if(i > startPos) { + const str = this.expression.substring(startPos, i) + if(this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) { + this.current = this.newToken(TOP, str) + this.pos += str.length + return true + } + } + return false + } + + /** + * Checks if the current character is an identifier and makes a variable, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isName() { + const startPos = this.pos + let i = startPos + let hasLetter = false + for(; i < this.expression.length; i++) { + const c = this.expression.charAt(i) + if(c.toUpperCase() === c.toLowerCase() && !ADDITIONAL_VARCHARS.includes(c)) { + if(i === this.pos && (c === "$" || c === "_")) { + if(c === "_") { + hasLetter = true + } + } else if(i === this.pos || !hasLetter || (c !== "_" && (c < "0" || c > "9"))) { + break + } + } else { + hasLetter = true + } + } + if(hasLetter) { + const str = this.expression.substring(startPos, i) + this.current = this.newToken(TNAME, str) + this.pos += str.length + return true + } + return false + } + + /** + * Checks if the character at the current position is a whitespace, and if so, consumes all consecutive whitespaces + * and returns true. Otherwise, returns false. + * @returns {boolean} + * + */ + isWhitespace() { + let r = false + let c = this.expression.charAt(this.pos) + while(c === " " || c === "\t" || c === "\n" || c === "\r") { + r = true + this.pos++ + if(this.pos >= this.expression.length) { + break + } + c = this.expression.charAt(this.pos) + } + return r + } + + /** + * Checks if the current character is a zero, and checks whether it forms a radix number, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isRadixInteger() { + let pos = this.pos + + if(pos >= this.expression.length - 2 || this.expression.charAt(pos) !== "0") { + return false + } + ++pos + + let radix + let validDigit + if(this.expression.charAt(pos) === "x") { + radix = 16 + validDigit = /^[0-9a-f]$/i + pos++ + } else if(this.expression.charAt(pos) === "b") { + radix = 2 + validDigit = /^[01]$/i + pos++ + } else { + return false + } + + let valid = false + const startPos = pos + + while(pos < this.expression.length) { + const c = this.expression.charAt(pos) + if(validDigit.test(c)) { + pos++ + valid = true + } else { + break + } + } + + if(valid) { + this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix)) + this.pos = pos + } + return valid + } + + /** + * Checks if the current character is a digit, and checks whether it forms a number, and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @returns {boolean} + */ + isNumber() { + const startPos = this.pos + let valid = false + let pos = startPos + let resetPos = startPos + let foundDot = false + let foundDigits = false + let c + + // Check for digit with dot. + while(pos < this.expression.length) { + c = this.expression.charAt(pos) + if((c >= "0" && c <= "9") || (!foundDot && c === ".")) { + if(c === ".") { + foundDot = true + } else { + foundDigits = true + } + pos++ + valid = foundDigits + } else { + break + } + } + + if(valid) { + resetPos = pos + } + + // Check for e exponents. + if(c === "e" || c === "E") { + pos++ + let acceptSign = true + let validExponent = false + while(pos < this.expression.length) { + c = this.expression.charAt(pos) + if(acceptSign && (c === "+" || c === "-")) { + acceptSign = false + } else if(c >= "0" && c <= "9") { + validExponent = true + acceptSign = false + } else { + break + } + pos++ + } + + if(!validExponent) { + pos = resetPos + } + } + + // Use parseFloat now that we've identified the number. + if(valid) { + this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos))) + this.pos = pos + } else { + this.pos = resetPos + } + return valid + } + + /** + * Checks if the current character is an operator, checks whether it's enabled and if so, consumes it as the current token + * and returns true. Otherwise, returns false. + * @return {boolean} + */ + isOperator() { + const startPos = this.pos + const c = this.expression.charAt(this.pos) + + if(c === "+" || c === "-" || c === "*" || c === "/" || c === "%" || c === "^" || c === "?" || c === ":" || c === ".") { + this.current = this.newToken(TOP, c) + } else if(c === "∙" || c === "•") { + this.current = this.newToken(TOP, "*") + } else if(c === ">") { + if(this.expression.charAt(this.pos + 1) === "=") { + this.current = this.newToken(TOP, ">=") + this.pos++ + } else { + this.current = this.newToken(TOP, ">") + } + } else if(c === "<") { + if(this.expression.charAt(this.pos + 1) === "=") { + this.current = this.newToken(TOP, "<=") + this.pos++ + } else { + this.current = this.newToken(TOP, "<") + } + } else if(c === "|") { + if(this.expression.charAt(this.pos + 1) === "|") { + this.current = this.newToken(TOP, "||") + this.pos++ + } else { + return false + } + } else if(c === "=") { + if(this.expression.charAt(this.pos + 1) === "=") { + this.current = this.newToken(TOP, "==") + this.pos++ + } else { + this.current = this.newToken(TOP, c) + } + } else if(c === "!") { + if(this.expression.charAt(this.pos + 1) === "=") { + this.current = this.newToken(TOP, "!=") + this.pos++ + } else { + this.current = this.newToken(TOP, c) + } + } else { + return false + } + this.pos++ + + if(this.isOperatorEnabled(this.current.value)) { + return true + } else { + this.pos = startPos + return false + } + } + + /** + * Replaces a backslash and a character by its unescaped value. + * @param {string} v - string to un escape. + */ + unescape(v) { + let index = v.indexOf("\\") + if(index < 0) { + return v + } + + let buffer = v.substring(0, index) + while(index >= 0) { + const c = v.charAt(++index) + switch(c) { + case "'": + buffer += "'" + break + case "\"": + buffer += "\"" + break + case "\\": + buffer += "\\" + break + case "/": + buffer += "/" + break + case "b": + buffer += "\b" + break + case "f": + buffer += "\f" + break + case "n": + buffer += "\n" + break + case "r": + buffer += "\r" + break + case "t": + buffer += "\t" + break + case "u": + // interpret the following 4 characters as the hex of the unicode code point + const codePoint = v.substring(index + 1, index + 5) + if(!unicodeCodePointPattern.test(codePoint)) { + this.parseError(qsTranslate("error", "Illegal escape sequence: %1.").arg("\\u" + codePoint)) + } + buffer += String.fromCharCode(parseInt(codePoint, 16)) + index += 4 + break + default: + throw this.parseError(qsTranslate("error", "Illegal escape sequence: %1.").arg("\\" + c)) + } + ++index + const backslash = v.indexOf("\\", index) + buffer += v.substring(index, backslash < 0 ? v.length : backslash) + index = backslash + } + + return buffer + } + + /** + * Shorthand for the parser's method to check if an operator is enabled. + * @param {string} op + * @return {boolean} + */ + isOperatorEnabled(op) { + return this.parser.isOperatorEnabled(op) + } + + /** + * Throws a translated error. + * @param {string} msg + */ + parseError(msg) { + throw new Error(qsTranslate("error", "Parse error [position %1]: %2").arg(this.pos).arg(msg)) + } +} diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs index 306f5be..ad4c1f2 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.mjs @@ -1,50 +1,52 @@ /** * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions. * Copyright (C) 2021-2024 Ad5001 - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import { Module } from '../modules.mjs' +import { Module } from "../modules.mjs" +import * as Instruction from "../lib/expr-eval/instruction.mjs" +import { escapeValue } from "../lib/expr-eval/expression.mjs" -const unicodechars = ["α","β","γ","δ","ε","ζ","η", - "π","θ","κ","λ","μ","ξ","ρ", - "ς","σ","τ","φ","χ","ψ","ω", - "Γ","Δ","Θ","Λ","Ξ","Π","Σ", - "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", - "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", - "ₜ","¹","²","³","⁴","⁵","⁶", - "⁷","⁸","⁹","⁰","₁","₂","₃", - "₄","₅","₆","₇","₈","₉","₀", +const unicodechars = ["α", "β", "γ", "δ", "ε", "ζ", "η", + "π", "θ", "κ", "λ", "μ", "ξ", "ρ", + "ς", "σ", "τ", "φ", "χ", "ψ", "ω", + "Γ", "Δ", "Θ", "Λ", "Ξ", "Π", "Σ", + "Φ", "Ψ", "Ω", "ₐ", "ₑ", "ₒ", "ₓ", + "ₕ", "ₖ", "ₗ", "ₘ", "ₙ", "ₚ", "ₛ", + "ₜ", "¹", "²", "³", "⁴", "⁵", "⁶", + "⁷", "⁸", "⁹", "⁰", "₁", "₂", "₃", + "₄", "₅", "₆", "₇", "₈", "₉", "₀", "pi", "∞"] -const equivalchars = ["\\alpha","\\beta","\\gamma","\\delta","\\epsilon","\\zeta","\\eta", - "\\pi","\\theta","\\kappa","\\lambda","\\mu","\\xi","\\rho", - "\\sigma","\\sigma","\\tau","\\phi","\\chi","\\psi","\\omega", - "\\Gamma","\\Delta","\\Theta","\\Lambda","\\Xi","\\Pi","\\Sigma", - "\\Phy","\\Psi","\\Omega","{}_{a}","{}_{e}","{}_{o}","{}_{x}", - "{}_{h}","{}_{k}","{}_{l}","{}_{m}","{}_{n}","{}_{p}","{}_{s}", - "{}_{t}","{}^{1}","{}^{2}","{}^{3}","{}^{4}","{}^{5}","{}^{6}", - "{}^{7}","{}^{8}","{}^{9}","{}^{0}","{}_{1}","{}_{2}","{}_{3}", - "{}_{4}","{}_{5}","{}_{6}","{}_{7}","{}_{8}","{}_{9}","{}_{0}", +const equivalchars = ["\\alpha", "\\beta", "\\gamma", "\\delta", "\\epsilon", "\\zeta", "\\eta", + "\\pi", "\\theta", "\\kappa", "\\lambda", "\\mu", "\\xi", "\\rho", + "\\sigma", "\\sigma", "\\tau", "\\phi", "\\chi", "\\psi", "\\omega", + "\\Gamma", "\\Delta", "\\Theta", "\\Lambda", "\\Xi", "\\Pi", "\\Sigma", + "\\Phy", "\\Psi", "\\Omega", "{}_{a}", "{}_{e}", "{}_{o}", "{}_{x}", + "{}_{h}", "{}_{k}", "{}_{l}", "{}_{m}", "{}_{n}", "{}_{p}", "{}_{s}", + "{}_{t}", "{}^{1}", "{}^{2}", "{}^{3}", "{}^{4}", "{}^{5}", "{}^{6}", + "{}^{7}", "{}^{8}", "{}^{9}", "{}^{0}", "{}_{1}", "{}_{2}", "{}_{3}", + "{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}", "\\pi", "\\infty"] /** * Class containing the result of a LaTeX render. - * + * * @property {string} source - Exported PNG file - * @property {number} width - * @property {number} height + * @property {number} width + * @property {number} height */ class LatexRenderResult { constructor(source, width, height) { @@ -56,7 +58,7 @@ class LatexRenderResult { class LatexAPI extends Module { constructor() { - super('Latex', [ + super("Latex", [ /** @type {ExprParserAPI} */ Modules.ExprParser ]) @@ -65,10 +67,10 @@ class LatexAPI extends Module { */ this.enabled = Helper.getSettingBool("enable_latex") } - + /** * Prepares and renders a latex string into a png file. - * + * * @param {string} markup - LaTeX markup to render. * @param {number} fontSize - Font size (in pt) to render. * @param {color} color - Color of the text to render. @@ -78,11 +80,11 @@ class LatexAPI extends Module { let args = Latex.render(markup, fontSize, color).split(",") return new LatexRenderResult(...args) } - + /** * Checks if the given markup (with given font size and color) has already been * rendered, and if so, returns its data. Otherwise, returns null. - * + * * @param {string} markup - LaTeX markup to render. * @param {number} fontSize - Font size (in pt) to render. * @param {color} color - Color of the text to render. @@ -95,14 +97,14 @@ class LatexAPI extends Module { ret = new LatexRenderResult(...data.split(",")) return ret } - + /** * Prepares and renders a latex string into a png file asynchronously. - * + * * @param {string} markup - LaTeX markup to render. * @param {number} fontSize - Font size (in pt) to render. * @param {color} color - Color of the text to render. - * @returns {Promize} + * @returns {Promise} */ requestAsyncRender(markup, fontSize, color) { return new Promise(resolve => { @@ -113,11 +115,11 @@ class LatexAPI extends Module { /** * Puts element within parenthesis. * - * @param {string} elem - element to put within parenthesis. + * @param {string|number} elem - element to put within parenthesis. * @returns {string} */ par(elem) { - return '(' + elem + ')' + return `(${elem})` } /** @@ -125,16 +127,16 @@ class LatexAPI extends Module { * the string array contents, but not at the first position of the string, * and returns the parenthesis version if so. * - * @param {string} elem - element to put within parenthesis. + * @param {string|number} elem - element to put within parenthesis. * @param {Array} contents - Array of elements to put within parenthesis. * @returns {string} */ parif(elem, contents) { elem = elem.toString() - if(elem[0] !== "(" && elem[elem.length-1] !== ")" && contents.some(x => elem.indexOf(x) > 0)) + if(elem[0] !== "(" && elem[elem.length - 1] !== ")" && contents.some(x => elem.indexOf(x) > 0)) return this.par(elem) - if(elem[0] === "(" && elem[elem.length-1] === ")") - return elem.substr(1, elem.length-2) + if(elem[0] === "(" && elem[elem.length - 1] === ")") + return elem.substr(1, elem.length - 2) return elem } @@ -149,31 +151,24 @@ class LatexAPI extends Module { switch(f) { case "derivative": if(args.length === 3) - return '\\frac{d' + args[0].substr(1, args[0].length-2).replace(new RegExp(args[1].substr(1, args[1].length-2), 'g'), 'x') + '}{dx}'; + return "\\frac{d" + args[0].substr(1, args[0].length - 2).replace(new RegExp(args[1].substr(1, args[1].length - 2), "g"), "x") + "}{dx}" else - return '\\frac{d' + args[0] + '}{dx}(x)'; - break; + return "\\frac{d" + args[0] + "}{dx}(x)" case "integral": if(args.length === 4) - return '\\int\\limits_{' + args[0] + '}^{' + args[1] + '}' + args[2].substr(1, args[2].length-2) + ' d' + args[3].substr(1, args[3].length-2); + return "\\int\\limits_{" + args[0] + "}^{" + args[1] + "}" + args[2].substr(1, args[2].length - 2) + " d" + args[3].substr(1, args[3].length - 2) else - return '\\int\\limits_{' + args[0] + '}^{' + args[1] + '}' + args[2] + '(t) dt'; - break; + return "\\int\\limits_{" + args[0] + "}^{" + args[1] + "}" + args[2] + "(t) dt" case "sqrt": - return '\\sqrt\\left(' + args.join(', ') + '\\right)'; - break; + return "\\sqrt\\left(" + args.join(", ") + "\\right)" case "abs": - return '\\left|' + args.join(', ') + '\\right|'; - break; + return "\\left|" + args.join(", ") + "\\right|" case "floor": - return '\\left\\lfloor' + args.join(', ') + '\\right\\rfloor'; - break; + return "\\left\\lfloor" + args.join(", ") + "\\right\\rfloor" case "ceil": - return '\\left\\lceil' + args.join(', ') + '\\right\\rceil'; - break; + return "\\left\\lceil" + args.join(", ") + "\\right\\rceil" default: - return '\\mathrm{' + f + '}\\left(' + args.join(', ') + '\\right)'; - break; + return "\\mathrm{" + f + "}\\left(" + args.join(", ") + "\\right)" } } @@ -188,150 +183,146 @@ class LatexAPI extends Module { if(wrapIn$) for(let i = 0; i < unicodechars.length; i++) { if(vari.includes(unicodechars[i])) - vari = vari.replace(new RegExp(unicodechars[i], 'g'), '$'+equivalchars[i]+'$') + vari = vari.replace(new RegExp(unicodechars[i], "g"), "$" + equivalchars[i] + "$") } else for(let i = 0; i < unicodechars.length; i++) { if(vari.includes(unicodechars[i])) - vari = vari.replace(new RegExp(unicodechars[i], 'g'), equivalchars[i]) + vari = vari.replace(new RegExp(unicodechars[i], "g"), equivalchars[i]) } - return vari; + return vari } /** - * Converts expr-eval tokens to a latex string. + * Converts expr-eval instructions to a latex string. * - * @param {Array} tokens - expr-eval tokens list + * @param {Instruction[]} instructions - expr-eval tokens list * @returns {string} */ - expression(tokens) { + expression(instructions) { let nstack = [] let n1, n2, n3 let f, args, argCount - for (let i = 0; i < tokens.length; i++) { - let item = tokens[i] + for(let item of instructions) { let type = item.type switch(type) { - case Modules.ExprParser.Internals.INUMBER: + case Instruction.INUMBER: if(item.value === Infinity) { nstack.push("\\infty") - } else if(typeof item.value === 'number' && item.value < 0) { - nstack.push(this.par(item.value)); + } else if(typeof item.value === "number" && item.value < 0) { + nstack.push(this.par(item.value)) } else if(Array.isArray(item.value)) { - nstack.push('[' + item.value.map(Modules.ExprParser.Internals.escapeValue).join(', ') + ']'); + nstack.push("[" + item.value.map(escapeValue).join(", ") + "]") } else { - nstack.push(Modules.ExprParser.Internals.escapeValue(item.value)); + nstack.push(escapeValue(item.value)) } - break; - case Modules.ExprParser.Internals.IOP2: - n2 = nstack.pop(); - n1 = nstack.pop(); - f = item.value; + break + case Instruction.IOP2: + n2 = nstack.pop() + n1 = nstack.pop() + f = item.value switch(f) { - case '-': - case '+': - nstack.push(n1 + f + n2); - break; - case '||': - case 'or': - case '&&': - case 'and': - case '==': - case '!=': - nstack.push(this.par(n1) + f + this.par(n2)); - break; - case '*': - if(n2 == "\\pi" || n2 == "e" || n2 == "x" || n2 == "n") - nstack.push(this.parif(n1,['+','-']) + n2) + case "-": + case "+": + nstack.push(n1 + f + n2) + break + case "||": + case "or": + case "&&": + case "and": + case "==": + case "!=": + nstack.push(this.par(n1) + f + this.par(n2)) + break + case "*": + if(n2 === "\\pi" || n2 === "e" || n2 === "x" || n2 === "n") + nstack.push(this.parif(n1, ["+", "-"]) + n2) else - nstack.push(this.parif(n1,['+','-']) + " \\times " + this.parif(n2,['+','-'])); - break; - case '/': - nstack.push("\\frac{" + n1 + "}{" + n2 + "}"); - break; - case '^': - nstack.push(this.parif(n1,['+','-','*','/','!']) + "^{" + n2 + "}"); - break; - case '%': - nstack.push(this.parif(n1,['+','-','*','/','!','^']) + " \\mathrm{mod} " + parif(n2,['+','-','*','/','!','^'])); - break; - case '[': - nstack.push(n1 + '[' + n2 + ']'); - break; + nstack.push(this.parif(n1, ["+", "-"]) + " \\times " + this.parif(n2, ["+", "-"])) + break + case "/": + nstack.push("\\frac{" + n1 + "}{" + n2 + "}") + break + case "^": + nstack.push(this.parif(n1, ["+", "-", "*", "/", "!"]) + "^{" + n2 + "}") + break + case "%": + nstack.push(this.parif(n1, ["+", "-", "*", "/", "!", "^"]) + " \\mathrm{mod} " + this.parif(n2, ["+", "-", "*", "/", "!", "^"])) + break + case "[": + nstack.push(n1 + "[" + n2 + "]") + break default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError("Unknown operator " + item.value + ".") } - break; - case Modules.ExprParser.Internals.IOP3: // Thirdiary operator - n3 = nstack.pop(); - n2 = nstack.pop(); - n1 = nstack.pop(); - f = item.value; - if (f === '?') { - nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')'); + break + case Instruction.IOP3: // Thirdiary operator + n3 = nstack.pop() + n2 = nstack.pop() + n1 = nstack.pop() + f = item.value + if(f === "?") { + nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")") } else { - throw new EvalError('Unknown operator ' + ope + '.'); + throw new EvalError("Unknown operator " + item.value + ".") } - break; - case Modules.ExprParser.Internals.IVAR: - case Modules.ExprParser.Internals.IVARNAME: - nstack.push(this.variable(item.value.toString())); - break; - case Modules.ExprParser.Internals.IOP1: // Unary operator - n1 = nstack.pop(); - f = item.value; + break + case Instruction.IVAR: + case Instruction.IVARNAME: + nstack.push(this.variable(item.value.toString())) + break + case Instruction.IOP1: // Unary operator + n1 = nstack.pop() + f = item.value switch(f) { - case '-': - case '+': - nstack.push(this.par(f + n1)); - break; - case '!': - nstack.push(this.parif(n1,['+','-','*','/','^']) + '!'); - break; + case "-": + case "+": + nstack.push(this.par(f + n1)) + break + case "!": + nstack.push(this.parif(n1, ["+", "-", "*", "/", "^"]) + "!") + break default: - nstack.push(f + this.parif(n1,['+','-','*','/','^'])); - break; + nstack.push(f + this.parif(n1, ["+", "-", "*", "/", "^"])) + break } - break; - case Modules.ExprParser.Internals.IFUNCALL: - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); + break + case Instruction.IFUNCALL: + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(nstack.pop()) } - f = nstack.pop(); + f = nstack.pop() // Handling various functions nstack.push(this.functionToLatex(f, args)) - break; - case Modules.ExprParser.Internals.IFUNDEF: - nstack.push(this.par(n1 + '(' + args.join(', ') + ') = ' + n2)); - break; - case Modules.ExprParser.Internals.IMEMBER: - n1 = nstack.pop(); - nstack.push(n1 + '.' + item.value); - break; - case Modules.ExprParser.Internals.IARRAY: - argCount = item.value; - args = []; - while (argCount-- > 0) { - args.unshift(nstack.pop()); + break + case Instruction.IMEMBER: + n1 = nstack.pop() + nstack.push(n1 + "." + item.value) + break + case Instruction.IARRAY: + argCount = item.value + args = [] + while(argCount-- > 0) { + args.unshift(nstack.pop()) } - nstack.push('[' + args.join(', ') + ']'); - break; - case Modules.ExprParser.Internals.IEXPR: - nstack.push('(' + this.expression(item.value) + ')'); - break; - case Modules.ExprParser.Internals.IENDSTATEMENT: - break; + nstack.push("[" + args.join(", ") + "]") + break + case Instruction.IEXPR: + nstack.push("(" + this.expression(item.value) + ")") + break + case Instruction.IENDSTATEMENT: + break default: - throw new EvalError('invalid Expression'); + throw new EvalError("invalid Expression") } } - if (nstack.length > 1) { - nstack = [ nstack.join(';') ] + if(nstack.length > 1) { + nstack = [nstack.join(";")] } - return String(nstack[0]); + return String(nstack[0]) } }