Nearly rewrote expr-eval to be compatible as an ECMAScript module, the last blocker for many bugs and JS tests!
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
commit f91538de446ef0e497ec7e87e2729f504a4172cb Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:55:12 2024 +0200 Converted expr-eval back to regular JS! commit 23c346f6c65b5b5c4bb4ad610f0554bd1d9a3700 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:06:59 2024 +0200 Reformatting commit 66608c980fd44f26ae8e6855ecd5fc3e7db55a7b Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 05:04:27 2024 +0200 Removed all 'var's commit 545886fd38c99cf11bc576caa40bec0d7fe0ac30 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:53:32 2024 +0200 Removing function definition commit 489602b24bb70cb6ad782871e269a22c92fcf072 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:51:21 2024 +0200 Removing semicolons commit 3ee069edbeb8ebfb5c7d15d319014f7a085ff623 Author: Ad5001 <mail@ad5001.eu> Date: Sun Sep 22 04:49:32 2024 +0200 Converting all classes to ECMAScript classes
This commit is contained in:
parent
1299fe469d
commit
cc0f277da7
12 changed files with 2340 additions and 2049 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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"
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,540 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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.<string, function(any): any>} unaryOps
|
||||
* @param {Record.<string, function(any, any): any>} binaryOps
|
||||
* @param {Record.<string, function(any, any, any): any>} ternaryOps
|
||||
* @param {Record.<string, any>} 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.<string, number>} 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<string, number|Expression>|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<string, number>} 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.<string, (Expression|string)>} 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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)
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.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(<from: number>, <to: number>, <f: ExecutableObject>)')
|
||||
let usage2 = qsTranslate('usage', 'integral(<from: number>, <to: number>, <f: string>, <variable: string>)')
|
||||
let usage1 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: ExecutableObject>)")
|
||||
let usage2 = qsTranslate("usage", "integral(<from: number>, <to: number>, <f: string>, <variable: string>)")
|
||||
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(<f: ExecutableObject>, <x: number>)')
|
||||
let usage2 = qsTranslate('usage', 'derivative(<f: string>, <variable: string>, <x: number>)')
|
||||
let usage1 = qsTranslate("usage", "derivative(<f: ExecutableObject>, <x: number>)")
|
||||
let usage2 = qsTranslate("usage", "derivative(<f: string>, <variable: string>, <x: number>)")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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.<string, boolean>, [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 (<condition> ? <value if true> : <value if false>) 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 (<condition 1> or <condition 2>) 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 (<condition 1> and <condition 2>) 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 (<condition 1> == <condition 2> 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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
|
||||
}
|
|
@ -0,0 +1,575 @@
|
|||
/**
|
||||
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
|
||||
* http://www.undefined.ch/mparser/index.html
|
||||
*
|
||||
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
|
||||
* https://silentmatt.com/javascript-expression-evaluator/
|
||||
*
|
||||
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (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))
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LatexRenderResult>}
|
||||
* @returns {Promise<LatexRenderResult>}
|
||||
*/
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue