From 666d611e95e53e3ca563cf7e9ee0a41b47ec11c8 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sat, 22 Oct 2022 14:06:03 +0200 Subject: [PATCH 1/4] Fixing up old stuff, cleaning up and standardizing AST. --- .../ad5001/LogarithmPlotter/js/math/latex.js | 41 +- .../ad5001/LogarithmPlotter/js/parsing/ast.js | 990 +++++++++++------- .../LogarithmPlotter/js/parsing/builder.js | 1 + .../LogarithmPlotter/js/parsing/reference.js | 12 +- 4 files changed, 645 insertions(+), 399 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js index 9ebdf6d..60bfa39 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/math/latex.js @@ -103,27 +103,28 @@ function functionToLatex(f, args) { * @param {string} vari - variable to convert * @returns {string} */ -function variable(vari) { - let unicodechars = ["α","β","γ","δ","ε","ζ","η", - "π","θ","κ","λ","μ","ξ","ρ", - "ς","σ","τ","φ","χ","ψ","ω", - "Γ","Δ","Θ","Λ","Ξ","Π","Σ", - "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", - "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", - "ₜ","¹","²","³","⁴","⁵","⁶", - "⁷","⁸","⁹","⁰","₁","₂","₃", - "₄","₅","₆","₇","₈","₉","₀", - "pi"] - let 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}", + +let unicodechars = ["α","β","γ","δ","ε","ζ","η", + "π","θ","κ","λ","μ","ξ","ρ", + "ς","σ","τ","φ","χ","ψ","ω", + "Γ","Δ","Θ","Λ","Ξ","Π","Σ", + "Φ","Ψ","Ω","ₐ","ₑ","ₒ","ₓ", + "ₕ","ₖ","ₗ","ₘ","ₙ","ₚ","ₛ", + "ₜ","¹","²","³","⁴","⁵","⁶", + "⁷","⁸","⁹","⁰","₁","₂","₃", + "₄","₅","₆","₇","₈","₉","₀", + "pi"] +let 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"] +function variable(vari) { for(let i = 0; i < unicodechars.length; i++) { //console.log(vari, unicodechars[i], equivalchars[i]); if(vari.includes(unicodechars[i])) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js index 30633b6..b00e297 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js @@ -8,7 +8,7 @@ * (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 + * 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. * @@ -19,10 +19,12 @@ .pragma library .import "reference.js" as Reference +.import "../math/latex.js" as Latex const DERIVATION_PRECISION = 0.01 +const ZERO_EPISLON = 5e-11 // Number under which a variable is considered 0 when dealing with floating point rounding errors. -const OPERATION_PRIORITY = { +const BINARY_OPERATION_PRIORITY = { "+": 10, "-": 10, "*": 20, "/": 20 } @@ -30,380 +32,599 @@ const OPERATION_PRIORITY = { enum ASEType { UNKNOWN, VARIABLE, + ARRAY, + PROPERTY, NUMBER, STRING, FUNCTION, CONSTANT, - OPERATION, - NEGATION // Example: -x. + UNARY_OPERATION, + BINARY_OPERATION, + TERTIARY_OPERATION, } +/** + * Base class for abstract syntax elements. + */ class AbstractSyntaxElement { - type = ASEType.UNKNOWN; + type = ASEType.UNKNOWN + /** + * Returns the computed number of value of the element + * depending on the given variables. + * + * @param {Dictionary} variables - variable name/value dictionary representing the variables. + * @throws {EvalError} When the expression is invalid or that variables are missing. + * @returns {number} + */ execute(variables) { - return null; + return null } + /** + * Simplifies to a maximum the current expression. + * + * @param {array} variables + * @returns {AbstractSyntaxElement} + */ simplify() { - return this; + throw new Error(`Function 'simplify' of ${this.type} not implemented.`) } - derivate(variable) { - return this; + /** + * Substitutes the given variable by another AbstractSyntaxElement. + * + * @param {string} variable + * @param {AbstractSyntaxElement} substitution + * @returns {AbstractSyntaxElement} + */ + substitute(variable, substitution) { + throw new Error(`Function 'substitute' of ${this.type} not implemented.`) } - integrate(variable) { - return this; + /** + * Returns the derivation of this element depending on a variable. + * WARNING: Might -or might not- clone the element. + * + * @param {string} variable + * @returns {AbstractSyntaxElement} + */ + derivation(variable) { + throw new Error(`Function 'derivation' of ${this.type} not implemented.`) } + /** + * Returns the integral of this element depending on a variable. + * WARNING: Might -or might not- clone the element. + * + * @param {string} variable + * @returns {AbstractSyntaxElement} + */ + integral(variable) { + throw new Error(`Function 'integral' of ${this.type} not implemented.`) + } + /** + * Returns the string that can be reparsed by the parser and be edited by the user. + * + * @returns {string} + */ toEditableString() { - return ""; + throw new Error(`Function 'toEditableString' of ${this.type} not implemented.`) } + /** + * Returns the LaTeX string of this item. + * + * @returns {string} + */ toLatex() { - return ""; + throw new Error(`Function 'toLatex' of ${this.type} not implemented.`) } + /** + * Checks whether the current item is constant or depends on variables. + * + * @returns {bool} + */ isConstant() { - return true; - } -} - -class Variable extends AbstractSyntaxElement { - type = ASEType.VARIABLE; - - constructor(variableName) { - this.varName = variableName; - } - - execute(variables) { - if(variables.includes(this.varName)) { - return variables[this.varName]; - } else { - throw new EvalError("Unknown variable " + this.varName + "."); - } - } - - derivate(variable) { - if(variable == this.varName) - return new NumberElement(1); - return this; - } - - integrate(variable) { - if(variable == this.varName) - // ^2/2 - return new Operation(new Operation(this, '^', new NumberElement(2)), '/', new NumberElement(2)); - return this; - } - - toEditableString() { - return this.varName; - } - - toLatex() { - return this.varName; - } - - isConstant() { - return false; - } -} - -class ArrayVariable extends Variable { - constructor(arrayName, astIndex) { - super(arrayName + "[" + astIndex.toEditableString() + "]") - this.arrayName = arrayName; - this.astIndex = astIndex; - } - - execute(variables) { - if(variables.includes(this.arrayName)) { - let index = this.astIndex.execute(variables) - if(index % 1 != 0 || index < 0) { // Float index. - throw new EvalError("Non-integer array index " + index + " used as array index for " + this.varName + "."); - } else if(variables[this.arrayName].length <= index) { - throw new EvalError("Out-of-range index " + index + " used as array index for " + this.varName + "."); - } else { - return variables[this.arrayName][index]; - } - } else { - throw new EvalError("Unknown variable " + this.varName + "."); - } - - toLatex() { - return this.varName; - } - } - - simplify() { - return new ArrayVariable(this.arrayName, this.astIndex.simplify()); - } - - toLatex() { - return this.arrayName + '\\left[' + this.astIndex.toLatex() + '\\right]'; - } - - isConstant() { - return false; - } -} - - -class Constant extends Variable { - type = ASEType.CONSTANT; - - constructor(constant) { - super(constant) - } - - execute(variables) { - if(Reference.CONSTANTS_LIST.includes(this.varName)) { - return Reference.CONSTANTS[this.varName]; - } else { - throw new EvalError("Unknown constant " + this.varName + "."); - } - } - - derivate(variable) { - if(variable == this.varName) - return new NumberElement(0); - return this; - } - - integrate(variable) { - return new Operation(new Variable(variable), '^', this); - } - - toEditableString() { - return this.varName; - } - - toLatex() { - return this.varName; - } - - isConstant() { - return true; + throw new Error(`Function 'isConstant' of ${this.type} not implemented.`) } } class NumberElement extends AbstractSyntaxElement { - type = ASEType.NUMBER; + type = ASEType.NUMBER constructor(number) { - this.value = parseFloat(number); + this.value = parseFloat(number) } - derivate(variable) { - return new NumberElement(0); + execute(variables) { + return this.value } - integrate(variable) { - return new Variable(variable); + simplify() { + return this + } + + subtitute(variable, substitution) { + return this + } + + derivation(variable) { + return new NumberElement(0) + } + + integral(variable) { + let v = new Variable(variable) + return this.value == 1 ? v : new BinaryOperation(this, '*', v) } toEditableString() { - return this.value.toString(); + return this.value.toString() } toLatex() { - return this.value.toString(); + return this.value == Infinity ? "\\infty" : this.value.toString() } isConstant() { - return true; + return true } } class StringElement extends AbstractSyntaxElement { - type = ASEType.STRING; + type = ASEType.STRING constructor(str) { - this.str = str; + this.str = str } execute(variables) { return this.str } - derivate(variable) { - return this; + simplify() { + return this } - integrate(variable) { - return this; + subtitute(variable, substitution) { + return this + } + + derivation(variable) { + return this + } + + integral(variable) { + return this } toEditableString() { - return '"' + this.str + '"'; + return '"' + this.str + '"' } toLatex() { - return this.str; + return this.str } isConstant() { - return true; + return true } } -class FunctionElement extends AbstractSyntaxElement { - type = ASEType.FUNCTION; +class Variable extends AbstractSyntaxElement { + type = ASEType.VARIABLE - constructor(functionName, astArguments) { - this.function = functionName; - this.args = astArguments; - } - - execute(variables) { - if(this.function == "derivate") { - return executeDerivative(variables) - } else if(this.function == "integrate") { - return executeIntegral(variables) - } else if(Reference.FUNCTIONS_LIST.includes(this.function)) { - let args = this.args.map(arg => arg.execute(variables)); - return Reference.FUNCTIONS[this.function](...args); - } else { - throw new EvalError("Unknown function " + this.function + "."); - } - } - - executeDerivative(variables) { - // Calculate derivation. - if(this.args.length == 2) - if(this.args[1] instanceof Variable) { - let d = this.args[1].varName; // derivative variable name. - if(Object.keys(variables).includes(d)) { - let plus = this.args[0].execute(Object.assign({}, variables, {d: variables[d]+DERIVATION_PRECISION/2})); - let min = this.args[0].execute(Object.assign({}, variables, {d: variables[d]-DERIVATION_PRECISION/2})); - return (plus-min)/DERIVATION_PRECISION - } else - throw new EvalError("Undefined variable " + d + "."); - } else - throw new EvalError(`Argument 1 of function derivate must be a variable.`) - else - throw new EvalError(`Function 'derivate' can only have 2 arguments. ${this.args.length} provided.`) - } - - executeIntegral(variables) { - // Calculate integral. - // Using simons rule - // https://en.wikipedia.org/wiki/Simpson%27s_rule - let d, f, a, b; - if(this.args.length == 2) - // Integral(f,var) integral of f by var. - if(this.args[1] instanceof Variable) - if(Object.keys(variables).includes(d)) { - d = this.args[1].varName; // derivative variable name. - if(!Object.keys(variables).includes(d)) - throw new EvalError("Undefined variable " + d + ".") - a = 0; - b = variables[d]; - f = this.args[0].execute; - } else - else - throw new EvalError(`Argument 2 of function derivate must be a variable.`) - else if(this.args.length == 4) - // Integral(a,b,f,var) integral from a to b of f by var. - if(this.args[3] instanceof Variable) - if(Object.keys(variables).includes(d)) { - a = this.args[0].execute(variables); - b = this.args[1].execute(variables); - f = this.args[2].execute; - d = this.args[3].varName; // derivative variable name. - if(!Object.keys(variables).includes(d)) - throw new EvalError("Undefined variable " + d + "."); - } - else - throw new EvalError(`Argument 4 of function derivate must be a variable.`) - else - throw new EvalError(`Function 'derivate' can only have 2 or 4 arguments. ${this.args.length} provided.`) - - // (b-a)/6*(f(a)+4*f((a+b)/2)+f(b)) - let f_a = f(Object.assign({}, variables, {d: a})), f_b = f(Object.assign({}, variables, {d: b})); - let f_m = f(Object.assign({}, variables, {d: (a+b)/2})) - return (b-a)/6*(f_a+4*f_m+f_b); + constructor(variableName) { + this.variableName = variableName } simplify() { - let args = this.args.map(arg => arg.simplify(variables)); - let newFunc = new FunctionElement(this.function, args); - let result; - if(newFunc.isConstant() && (result = newFunc.execute({})) % 1 == 0) { // Simplification (e.g. cos(0), sin(π/2)...) - return new NumberElement(result); - } else { - return newFunc; - } + return this } - derivate(variable) { - //TODO: Use DERIVATIVES elements in reference. - return new FunctionElement("derivate", this, variable); + subtitute(variable, substitution) { + return variable == this.variableName ? substitution : this } - integrate(variable) { - //TODO: Use INTEGRALS elements in reference. - return new FunctionElement("integrate", this, variable); + execute(variables) { + if(this.variableName in variables) + return variables[this.variableName] + else + throw new EvalError(`Unknown variable ${this.variableName}.`) + } + + derivation(variable) { + return new NumberElement(variable == this.variableName ? 1 : 0) + } + + integral(variable) { + let op = new BinaryOperation(this, '*', new Variable(variable)) + if(variable == this.variableName) + // ^2/2 + op = new BinaryOperation(new BinaryOperation(this, '^', new NumberElement(2)), '/', new NumberElement(2)) + return op } toEditableString() { - return this.function + '(' + this.args.map(arg => arg.toEditableString()).join(', ') + ')'; + return this.variableName + } + + toLatex() { + return Latex.variable(this.variableName) + } + + isConstant() { + return false + } +} + +class Constant extends Variable { + type = ASEType.CONSTANT + + constructor(constant) { + super(constant) + } + + execute(variables) { + if(Reference.CONSTANTS_LIST.includes(this.variableName)) + return Reference.CONSTANTS[this.variableName] + else + throw new EvalError(`Unknown constant ${this.variableName}.`) + } + + isConstant() { + return true + } +} + +class ArrayElement extends AbstractSyntaxElement { + type = ASEType.ARRAY + + constructor(astArrayElement, astIndex) { + this.arrayFormula = astArrayElement.toEditableString() + this.astArrayElement = astArrayElement + this.astIndex = astIndex + } + + execute(variables) { + let elem = this.astArrayElement.execute(variables) + let index = this.astIndex.execute(variables) + if(Array.isArray(elem)) { + if(index % 1 != 0 || index < 0) { // Float index. + throw new EvalError(`Non-integer array index ${index} used for ${this.arrayFormula}.`) + } else if(elem.length <= index) { + throw new EvalError(`Out-of-range array index ${index} used for ${this.arrayFormula} (has ${elem.length} elements).`) + } else { + return elem[index] + } + } else + throw new EvalError(`${this.arrayFormula} is not an array.`) + } + + simplify() { + return new ArrayElement( + this.astArrayElement.simplify(), + this.astIndex.simplify() + ) + } + + substitute(variable, substitution) { + return new ArrayElement( + this.astArrayElement.substitute(variable, substitution), + this.astIndex.substitute(variable, substitution) + ) + } + + derivation(variable) { + return new NumberElement(0) + // TODO: Implement derivation depending on value. + } + + integral(variable) { + return new BinaryOperation(this,'*',new Variable(variable)) + // TODO: Implement integral depending on value. + } + + toEditableString() { + return `${this.arrayFormula}[${this.astIndex.toEditableString()}]` + } + + toLatex() { + return `${this.astArrayElement.toLatex()}\\left[${this.astIndex.toLatex()}\\right]` + } + + isConstant() { + return this.astArrayElement.isConstant() && this.astIndex.isConstant() + } +} + +class PropertyElement extends AbstractSyntaxElement { + type = ASEType.PROPERTY + + constructor(astObjectElement, propertyName) { + this.astObjectFormula = astObjectElement.toEditableString() + this.astObjectElement = astObjectElement + this.propertyName = propertyName + } + + execute(variables) { + let elem = this.astObjectElement.execute(variables) + if(typeof elem == 'object') { + if(this.propertyName in elem) + return elem[propertyName] + else + throw new EvalError(`Property ${propertyName} of ${this.astObjectFormula} does not exist.`) + } else + throw new EvalError(`${this.astObjectFormula} is not an object.`) + } + + simplify() { + return new PropertyElement(this.astObjectElement.simplify(), this.propertyName) + } + + substitute(variable, substitution) { + return new PropertyElement( + this.astObjectElement.substitute(variable, substitution), this.propertyName + ) + } + + derivation(variable) { + return new NumberElement(0) + // TODO: Implement derivation depending on value. + } + + integral(variable) { + return new BinaryOperation(this,'*',new Variable(variable)) + // TODO: Implement integral depending on value. + } + + toEditableString() { + return `${this.astObjectFormula}.${this.propertyName}` + } + + toLatex() { + return `${this.astObjectFormula.toLatex()}.${this.propertyName}` + } + + isConstant() { + return this.astObjectFormula.isConstant() + } +} + +/** + * Base class for all functions EXCEPT integral and derivation (see subclasses) + * TODO: Implement function name as elements to have property functions. + **/ +class FunctionElement extends AbstractSyntaxElement { + type = ASEType.FUNCTION + + constructor(functionName, astArguments) { + this.function = functionName + this.args = astArguments + } + + execute(variables) { + if(Reference.FUNCTIONS_LIST.includes(this.function)) { + let args = this.args.map(arg => arg.execute(variables)) + return Reference.FUNCTIONS[this.function](...args) + } else + throw new EvalError(`Unknown function ${this.function}.`) + } + + simplify() { + let args = this.args.map(arg => arg.simplify(variables)) + let newFunc = new FunctionElement(this.function, args) + let result = newFunc + if(newFunc.isConstant() && (result = newFunc.execute({})) % 1 < ZERO_EPISLON) + // Prevent simplification of non constants (e.g. non constant cos, sin...) + newFunc = new NumberElement(result) + return newFunc + } + + substitute(variable, substitution) { + } + + derivation(variable) { + //TODO: Use DERIVATIVES elements in reference. + return new DerivationElement([this, variable]) + } + + integral(variable) { + //TODO: Use INTEGRALS elements in reference. + return new IntegralElement([this, variable]) + } + + toEditableString() { + return `${this.function}(${this.args.map(arg => arg.toEditableString()).join(', ')})` } toLatex() { switch(this.function) { case "sqrt": - return '\\sqrt{' + this.args.map(arg => arg.toLatex()).join(', ') + '}'; + return '\\sqrt{' + this.args.map(arg => arg.toLatex()).join(', ') + '}' case "abs": - return '\\left|' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right|'; + return '\\left|' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right|' case "floor": - return '\\left\\lfloor' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rfloor'; + return '\\left\\lfloor' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rfloor' case "ceil": - return '\\left\\lceil' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rceil'; + return '\\left\\lceil' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rceil' default: - return '\\mathrm{' + this.function + '}\\left(' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right)'; + return '\\mathrm{' + this.function + '}\\left(' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right)' } } isConstant() { - if(this.function == "derivate") - return this.args[0].isConstant(); - else if(this.function == "integrate") - return this.args.length == 4 && this.args[0].isConstant() && this.args[1].isConstant() && this.args[2].isConstant(); - else - return this.args.every(x => x.isConstant()); + return this.args.every(x => x.isConstant()) } } -class Operation extends AbstractSyntaxElement { - type = ASEType.OPERATION; +/** + * Signatures supported for derivation: + * - derivation(f,var) + **/ +class DerivationElement extends FunctionElement { + + constructor(astArguments) { + super("derivation", astArguments) + this.args = astArguments + // Check syntax + if(this.args.length != 2) + throw new Error(`Function 'derivation' can only have 2 arguments. ${this.args.length} provided.`) + if(!(this.args[1] instanceof Variable)) + throw new Error(`Argument 1 of function 'derivation' must be a variable.`) + } + + execute(variables) { + // Calculate derivation. + // TODO: Do derivation simplification. + let d = this.args[1].variableName // derivation variable name. + if(d in variables) { + let plus = this.args[0].execute(Object.assign( + {[d]: variables[d]+DERIVATION_PRECISION/2}, variables + )) + let min = this.args[0].execute(Object.assign( + {[d]: variables[d]-DERIVATION_PRECISION/2}, variables + )) + return (plus-min)/DERIVATION_PRECISION + } else + throw new EvalError(`Undefined variable ${d}.`) + + } + + simplify() { + return new DerivationElement([this.args[0].simplify(variables), this.args[1]]) + } + + integral(variable) { + // Check if we're integrating and derivating by the same variable + return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable) + } + + toLatex() { + return `\\frac{d(${this.args[0].toLatex()})}{d${this.args[1].toLatex()}}` + } + + isConstant() { + return this.args[0].isConstant() + } +} + +/** + * Signatures supported for integrals: + * - integral(f,var) + * - integral(a,b,f,var) + **/ +class IntegralElement extends FunctionElement { + + constructor(astArguments) { + super("integral", astArguments) + this.args = astArguments + // Check syntax + if(![2,4].includes(this.args.length)) + throw new Error(`Function 'integral' can only have 2 or 4 arguments. ${this.args.length} provided.`) + if(!(this.args[this.args.length-1] instanceof Variable)) + // Last argument must always be a variable + throw new Error(`Argument ${this.args.length} of function 'integral' must be a variable.`) + // Setting shortcuts so that we don't have to if every time. + if(this.args.length == 2) { + this.a = new NumberElement(0) + this.b = new Variable('x') + this.f = args[0] + this.d = args[1] + } else { + [this.a, this.b, this.f, this.d] = args + } + } + + execute(variables) { + // Calculate integral. + // Using Simpsons rule + // https://en.wikipedia.org/wiki/Simpson%27s_rule + let a = this.a.execute(variables) + let b + try { + b = this.b.execute(variables) + } catch(e) { + if(this.args.length == 2) + throw new EvalError(`Cannot integrate ${this.args[0].toEditableString()}: no limits were defined and x is not a variable.`) + else + throw e + } + let f = this.f.execute + let d = this.d.variableName + + // (b-a)/6*(f(a)+4*f((a+b)/2)+f(b)) + let f_a = f(Object.assign({[d]: a}, variables)) + let f_b = f(Object.assign({[d]: b}, variables)) + let f_m = f(Object.assign({[d]: (a+b)/2}, variables)) + return (b-a)/6*(f_a+4*f_m+f_b) + } + + simplify() { + // TODO: When full derivation and integrals are implemented, use dedicated functions for simplification. + let func = this.args[this.args.length-2].simplify(variables) + let newElem + if(func.isConstant() && this.args.length == 4) + // Simplify integral. + newElem = new BinaryOperation( + new BinaryOperation(this.args[1], '-', this.args[0]).simplify(), + '*', + func + ) + else + newElem = new IntegralElement(this.args.length == 4 ? + [this.a.simplify(), this.b.simplify(), func, this.d] : + [func, this.d] + ) + return newElem + } + + derivation(variable) { + // Check if we're integrating and derivating by the same variable + return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable) + } + + toLatex() { + let limits = this.args.length == 2 ? '' : + `\\limits_{${this.b.toLatex()}}^{${this.b.toLatex()}}` + return `\\int${limits}{${this.f.toLatex()}}{d${this.d.toLatex()}}` + } + + isConstant() { + return this.a.isConstant() && this.b.isConstant() && this.f.isConstant() + } +} + +class BinaryOperation extends AbstractSyntaxElement { + type = ASEType.BINARY_OPERATION constructor(leftHand, operation, rightHand) { - this.leftHand = leftHand; - this.ope = operation; - this.rightHand = rightHand; + this.leftHand = leftHand + this.ope = operation + this.rightHand = rightHand } evaluate(variables) { switch(this.ope) { case '+': - return this.leftHand.evaluate(variables) + this.rightHand.evaluate(variables); + return this.leftHand.evaluate(variables) + this.rightHand.evaluate(variables) case '-': - return this.leftHand.evaluate(variables) - this.rightHand.evaluate(variables); + return this.leftHand.evaluate(variables) - this.rightHand.evaluate(variables) case '*': - return this.leftHand.evaluate(variables) * this.rightHand.evaluate(variables); + return this.leftHand.evaluate(variables) * this.rightHand.evaluate(variables) case '/': - return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables); + return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables) case '%': - return this.leftHand.evaluate(variables) % this.rightHand.evaluate(variables); + return this.leftHand.evaluate(variables) % this.rightHand.evaluate(variables) case '^': - return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables)); + return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables)) default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError("Unknown operator " + ope + ".") } } simplify() { - let leftHand = this.leftHand.simplify(); - let rightHand = this.rightHand.simplify(); - let newOpe = new Operation(leftHand, this.ope, rightHand); - if(leftHand.isConstant() && rightHand.isConstant() && Math.abs(newOpe.execute({})) < 1000000) { + let leftHand = this.leftHand.simplify() + let rightHand = this.rightHand.simplify() + let newOpe = new BinaryOperation(leftHand, this.ope, rightHand) + let result + let tmpResult + if(newOpe.isConstant() && (tmpResult = Math.abs(newOpe.execute({})) < 1000000)) { // Do not simplify to too big numbers switch(this.ope) { case '+': @@ -411,122 +632,145 @@ class Operation extends AbstractSyntaxElement { case '*': case '^': case '%': - return new NumberElement(newOpe.execute({})); + result = new NumberElement(tmpResult) + break case '/': - if(result % 1 == 0) - return new NumberElement(newOpe.execute({})); + if(tmpResult % 1 == 0) + result = new NumberElement(tmpResult) else { let simplified = simplifyFraction(leftHand.number, rightHand.number) - return new Operation(new NumberElement(simplified[0]), '/', new NumberElement(simplified[1])) + result = new BinaryOperation(new NumberElement(simplified[0]), '/', new NumberElement(simplified[1])) } - return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables); - return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables)); + break default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError("Unknown operator " + ope + ".") } } else { // Simplifications of +- 0 or *1 switch(this.ope) { case '+': case '-': - if(leftHand.type == ASEType.NUMBER && leftHand.value == 0) - return rightHand; - else if(rightHand.type == ASEType.NUMBER && rightHand.value == 0) { - if(ope == '-') leftHand.value = -leftHand.value; - return leftHand; + if(leftHand instanceof NumberElement && leftHand.value == 0) + return rightHand + else if(rightHand instanceof NumberElement && rightHand.value == 0) { + if(ope == '-') leftHand.value = -leftHand.value + result = leftHand } else - return newOpe + result = newOpe + break case '*': - if((leftHand.type == ASEType.NUMBER && leftHand.value == 0) || (rightHand.type == ASEType.NUMBER && rightHand.value == 0)) - return new NumberElement(0); - else if(leftHand.type == ASEType.NUMBER && leftHand.value == 1) - return rightHand; - else if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) - return leftHand; + if((leftHand instanceof NumberElement && leftHand.value == 0) || (rightHand instanceof NumberElement && rightHand.value == 0)) + result = new NumberElement(0) + else if(leftHand instanceof NumberElement && leftHand.value == 1) + result = rightHand + else if(rightHand instanceof NumberElement && rightHand.value == 1) + result = leftHand else - return newOpe + result = newOpe + break case '^': - if(rightHand.type == ASEType.NUMBER && rightHand.value == 0) - return new NumberElement(1); - else if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) - return new NumberElement(leftHand.value); + if(rightHand instanceof NumberElement && rightHand.value == 0) + result = new NumberElement(1) + else if(rightHand instanceof NumberElement && rightHand.value == 1) + result = new NumberElement(leftHand.value) else - return newOpe; + result = newOpe + break case '/': - if(rightHand.type == ASEType.NUMBER && rightHand.value == 1) - return new NumberElement(leftHand.value); + if(rightHand instanceof NumberElement && rightHand.value == 1) + result = new NumberElement(leftHand.value) else - return newOpe; + result = newOpe + break case '%': - return newOpe; + result = newOpe + break default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError("Unknown operator " + ope + ".") } } + return result } - derivate(variable) { + substitute(variable, substitution) { + return new BinaryOperation( + this.leftHand.substitute(variable, substitution), + this.ope, + this.rightHand.substitute(variable, substitution) + ) + } + + derivation(variable) { switch(this.ope) { case '-': case '+': - return new Operation(this.leftHand.derivate(variable), this.ope, this.rightHand.derivate(variable)); + return new BinaryOperation(this.leftHand.derivation(variable), this.ope, this.rightHand.derivation(variable)) case '*': - return new Operation( - new Operation(this.leftHand, '*', this.rightHand.derivate(variable)), + // (f*g)' = f'g + fg' + return new BinaryOperation( + new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)), '+', - new Operation(this.leftHand.derivate(variable), '*', this.rightHand), - ); + new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand), + ) case '/': - return new Operation( - new Operation( - new Operation(this.leftHand, '*', this.rightHand.derivate(variable)), + // (f/g)' = (f'g - fg')/g^2 + return new BinaryOperation( + new BinaryOperation( + new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)), '-', - new Operation(this.leftHand.derivate(variable), '*', this.rightHand), + new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand), ), '/', - new Operation(this.rightHand, '^', new NumberElement(2)) - ); + new BinaryOperation(this.rightHand, '^', new NumberElement(2)) + ) case '^': + // f^g = e^gln(f) => (e^gln(f))' = (gln(f))'e^gln(f) + // = (gln'(f) + g'ln(f))e^gln(f) + // = (gf'/f + g'ln(f))e^gln(f) + // Bit of a pain to implement, not really worth in terms of 'simplification' for synthesis. + // So I don't use it here. case '%': - return new FunctionElement("derivate", this.toEditableString()); + return new DerivationElement([this, new Variable(variable)]) default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError(`Unknown operator ${ope}.`) } } - integrate(variable) { + integral(variable) { switch(this.ope) { case '-': case '+': - return new Operation(this.leftHand.integrate(variable), this.ope, this.rightHand.integrate(variable)); + return new BinaryOperation(this.leftHand.integral(variable), this.ope, this.rightHand.integral(variable)) case '*': - return new Operation( - new Operation(this.leftHand.derivate(variable), '*', this.rightHand), + return new BinaryOperation( + new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand), '+', - new Operation(this.leftHand, '*', this.rightHand.derivate(variable)) - ); + new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)) + ) case '/': - return new Operation( - new Operation(this.leftHand.derivate(variable), '*', this.rightHand), + return new BinaryOperation( + new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand), '+', - new Operation(this.leftHand, '*', this.rightHand.derivate(variable)) - ); + new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)) + ) case '^': case '%': - return new FunctionElement("integrate", this.toEditableString()); + return new IntegralElement("integral", this.toEditableString()) default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError(`Unknown operator ${ope}.`) } } toEditableString() { - let leftString = this.leftHand.toEditableString(); - let rightString = this.rightHand.toEditableString(); - if(this.leftHand.type == ASEType.OPERATION && OPERATION_PRIORITY[this.ope] > OPERATION_PRIORITY[this.leftHand.ope]) + let leftString = this.leftHand.toEditableString() + let rightString = this.rightHand.toEditableString() + if(this.leftHand.type == ASEType.BINARY_OPERATION && + BINARY_OPERATION_PRIORITY[this.ope] > BINARY_OPERATION_PRIORITY[this.leftHand.ope]) leftString = "(" + leftString + ")" - if(this.rightHand.type == ASEType.OPERATION && OPERATION_PRIORITY[this.ope] > OPERATION_PRIORITY[this.rightHand.ope]) + if(this.rightHand.type == ASEType.BINARY_OPERATION && + BINARY_OPERATION_PRIORITY[this.ope] > BINARY_OPERATION_PRIORITY[this.rightHand.ope]) rightString = "(" + rightString + ")" - return leftString + " " + this.ope + " " + rightString; + return `${leftString} ${this.ope} ${rightString}` } @@ -534,130 +778,130 @@ class Operation extends AbstractSyntaxElement { switch(this.ope) { case '-': case '+': - return this.leftHand.toLatex() + this.ope + this.rightHand.toLatex(); + return this.leftHand.toLatex() + this.ope + this.rightHand.toLatex() case '*': - return this.leftHand.toLatex() + " \\times " + this.rightHand.toLatex(); + return this.leftHand.toLatex() + " \\times " + this.rightHand.toLatex() case '%': - return this.leftHand.toLatex() + " \\mathrm{mod} " + this.rightHand.toLatex(); + return this.leftHand.toLatex() + " \\mathrm{mod} " + this.rightHand.toLatex() case '/': return "\\frac{" + this.leftHand.toLatex() + "}{" + this.rightHand.toLatex() + "}" case '^': - return this.leftHand.toLatex() + "^{" + this.rightHand.toLatex() + "}"; + return this.leftHand.toLatex() + "^{" + this.rightHand.toLatex() + "}" default: - throw new EvalError("Unknown operator " + ope + "."); + throw new EvalError("Unknown operator " + ope + ".") } - return this.leftHand.toLatex() + ope + this.rightHand.toLatex(); + return this.leftHand.toLatex() + ope + this.rightHand.toLatex() } isConstant() { - return this.leftHand.isConstant() && this.rightHand.isConstant(); + return this.leftHand.isConstant() && this.rightHand.isConstant() } } function simplifyFraction(num,den) { // More than gcd because it allows decimals fractions. - let mult = 1; + let mult = 1 if(num%1 != 0) mult = Math.max(mult,Math.pow(10,num.toString().split('.')[1].length)) else if(den%1 != 0) mult = Math.max(mult,Math.pow(10,den.toString().split('.')[1].length)) - let a = Math.abs(num*mult); - let b = Math.abs(den*mult); + let a = Math.abs(num*mult) + let b = Math.abs(den*mult) let gcd = 0 if (b > a) {let temp = a; a = b; b = temp;} while (gcd == 0) { - if (b == 0) gcd = a; - a %= b; - if (a == 0) gcd = b; - b %= a; + if (b == 0) gcd = a + a %= b + if (a == 0) gcd = b + b %= a } return [num*mult/gcd, den*mult/gcd] } class Negation extends AbstractSyntaxElement { - type = ASEType.NEGATION; + type = ASEType.NEGATION constructor(variableName) { - this.varName = variableName; + this.variableName = variableName } execute(variables) { - if(variables.includes(this.varName)) { - return variables[this.varName]; + if(this.variableName in variables) { + return variables[this.variableName] } else { - throw new EvalError("Unknown variable " + this.varName + "."); + throw new EvalError("Unknown variable " + this.variableName + ".") } } - derivate(variable) { - if(variable == this.varName) - return new NumberElement(1); - return this; + derivation(variable) { + if(variable == this.variableName) + return new NumberElement(1) + return this } - integrate(variable) { - if(variable == this.varName) + integral(variable) { + if(variable == this.variableName) // ^2/2 - return new Operation(new Operation(this, '^', new NumberElement(2)), '/', new NumberElement(2)); - return this; + return new BinaryOperation(new BinaryOperation(this, '^', new NumberElement(2)), '/', new NumberElement(2)) + return this } toEditableString() { - return this.varName; + return this.variableName } toLatex() { - return this.varName; + return this.variableName } isConstant() { - return false; + return false } } class Negation extends AbstractSyntaxElement { - type = ASEType.NEGATION; + type = ASEType.NEGATION constructor(expression) { - this.expression = expression; + this.expression = expression } execute(variables) { if(variables.includes(this.arrayName)) { let index = this.astIndex.execute(variables) if(index % 1 != 0 || index < 0) { // Float index. - throw new EvalError("Non-integer array index " + index + " used as array index for " + this.varName + "."); + throw new EvalError("Non-integer array index " + index + " used as array index for " + this.variableName + ".") } else if(variables[this.arrayName].length <= index) { - throw new EvalError("Out-of-range index " + index + " used as array index for " + this.varName + "."); + throw new EvalError("Out-of-range index " + index + " used as array index for " + this.variableName + ".") } else { - return variables[this.arrayName][index]; + return variables[this.arrayName][index] } } else { - throw new EvalError("Unknown variable " + this.varName + "."); + throw new EvalError("Unknown variable " + this.variableName + ".") } toLatex() { - return this.varName; + return this.variableName } } simplify() { - return new Negation(this.expression.simplify()); + return new Negation(this.expression.simplify()) } - derivate(variable) { - return new Negation(this.expression.derivate(variable)); + derivation(variable) { + return new Negation(this.expression.derivation(variable)) } - integrate(variable) { - return new Negation(this.expression.integrate(variable)); + integral(variable) { + return new Negation(this.expression.integral(variable)) } toLatex() { - return '-' + this.expression.toLatex(); + return '-' + this.expression.toLatex() } isConstant() { - return this.expression.isConstant(); + return this.expression.isConstant() } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js index b81b6e1..4c9a0ce 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js @@ -25,6 +25,7 @@ class ExpressionBuilder { constructor(tokenizer) { this.tokenizer = tokenizer; + this.stack = [] } parseExpression(delimitors = '') { diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js index 81470f2..e92bc08 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js @@ -71,15 +71,15 @@ const FUNCTIONS_LIST = Object.keys(FUNCTIONS); // TODO: Complete const DERIVATIVES = { "abs": "abs(<1>)/<1>", - "acos": "-derivate(<1>)/sqrt(1-(<1>)^2)", - "acosh": "derivate(<1>)/sqrt((<1>)^2-1)", - "asin": "derivate(<1>)/sqrt(1-(<1>)^2)", - "asinh": "derivate(<1>)/sqrt((<1>)^2+1)", - "atan": "derivate(<1>)/(1+(<1>)^2)", + "acos": "-derivative(<1>)/sqrt(1-(<1>)^2)", + "acosh": "derivative(<1>)/sqrt((<1>)^2-1)", + "asin": "derivative(<1>)/sqrt(1-(<1>)^2)", + "asinh": "derivative(<1>)/sqrt((<1>)^2+1)", + "atan": "derivative(<1>)/(1+(<1>)^2)", "atan2": "", } const INTEGRALS = { - "abs": "integrate(<1>)*sign(<1>)", + "abs": "integral(<1>)*sign(<1>)", "acos": "", "acosh": "", "asin": "", From d7704110dddc0772e1d2a999172d82bebcee7e9b Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sun, 23 Oct 2022 15:43:47 +0200 Subject: [PATCH 2/4] Lots of changes to the AST Builder. Adding IDENTIFIER Token type with optional differentiation between functions, constants and variables for syntax highlighting. Adding substitute for many AST elements. Properly starting builder: parsing identifiers, functions, variables, array values, properties, numbers, strings and subexpressions. --- .../Setting/ExpressionEditor.qml | 2 +- .../ad5001/LogarithmPlotter/js/parsing/ast.js | 59 +++-- .../LogarithmPlotter/js/parsing/builder.js | 223 ++++++++++++++++-- .../LogarithmPlotter/js/parsing/reference.js | 15 ++ .../LogarithmPlotter/js/parsing/tokenizer.js | 45 +++- 5 files changed, 297 insertions(+), 47 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml index 34cc4b3..c587fcd 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/Setting/ExpressionEditor.qml @@ -504,7 +504,7 @@ Item { Generates a list of tokens from the given. */ function tokens(text) { - let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, false) + let tokenizer = new Parsing.Tokenizer(new Parsing.Input(text), true, true, false) let tokenList = [] let token while((token = tokenizer.next()) != null) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js index b00e297..160ebfb 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js @@ -24,10 +24,7 @@ const DERIVATION_PRECISION = 0.01 const ZERO_EPISLON = 5e-11 // Number under which a variable is considered 0 when dealing with floating point rounding errors. -const BINARY_OPERATION_PRIORITY = { - "+": 10, "-": 10, - "*": 20, "/": 20 -} +const BINARY_OPERATION_PRIORITY = Reference.BINARY_OPERATION_PRIORITY enum ASEType { UNKNOWN, @@ -417,16 +414,17 @@ class FunctionElement extends AbstractSyntaxElement { } substitute(variable, substitution) { + return new FunctionElement(this.functionName, this.args.map(arg => arg.substitute(variable, substitution))) } derivation(variable) { //TODO: Use DERIVATIVES elements in reference. - return new DerivationElement([this, variable]) + return new DerivationElement([this, new Variable(variable)]) } integral(variable) { //TODO: Use INTEGRALS elements in reference. - return new IntegralElement([this, variable]) + return new IntegralElement([this, new Variable(variable)]) } toEditableString() { @@ -487,12 +485,20 @@ class DerivationElement extends FunctionElement { } simplify() { - return new DerivationElement([this.args[0].simplify(variables), this.args[1]]) + return this.args[0].simplify().derivation(this.args[1].variableName).simplify() + } + + substitute(variable, substitution) { + if(variable == this.args[1].variableName) { + // Simplifu, + return this.simplify().substitute(variable, substitution) + } else + return new DerivationElement([this.args[0].substitute(variable, substitution), this.args[1]]) } integral(variable) { // Check if we're integrating and derivating by the same variable - return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable) + return variable == this.args[1].variableName ? this.args[1] : super(variable) } toLatex() { @@ -557,26 +563,44 @@ class IntegralElement extends FunctionElement { simplify() { // TODO: When full derivation and integrals are implemented, use dedicated functions for simplification. - let func = this.args[this.args.length-2].simplify(variables) + let func = this.f.simplify(variables) let newElem if(func.isConstant() && this.args.length == 4) // Simplify integral. newElem = new BinaryOperation( - new BinaryOperation(this.args[1], '-', this.args[0]).simplify(), + new BinaryOperation(this.b, '-', this.a).simplify(), '*', func - ) - else - newElem = new IntegralElement(this.args.length == 4 ? - [this.a.simplify(), this.b.simplify(), func, this.d] : - [func, this.d] - ) + ).simplify() + else { + let integrated = this.func.integral(this.d.variableName) + newElem = new BinaryOperation( + integrated.substitute(this.d.variableName, this.b), + '-', + integrated.substitute(this.d.variableName, this.a) + ).simplify() + //newElem = new IntegralElement(this.args.length == 4 ? + // [this.a.simplify(), this.b.simplify(), func, this.d] : + // [func, this.d] + //) + } return newElem } + substitute(variable, substitution) { + if(variable == this.args[1].variableName) { + // Simplify + return this.simplify().substitute(variable, substitution) + } else + return new IntegralElement(this.args.length == 4 ? + [this.a.substitute(variable, substitution), this.b.simplify(variable, substitution), + this.f.substitute(variable, substitution), this.d] : + [this.f.substitute(variable, substitution), this.d]) + } + derivation(variable) { // Check if we're integrating and derivating by the same variable - return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable) + return variable == this.args[1].variableName ? this.args[1] : super(variable) } toLatex() { @@ -689,6 +713,7 @@ class BinaryOperation extends AbstractSyntaxElement { throw new EvalError("Unknown operator " + ope + ".") } } + // TODO: Check for all nearby operations simplifications return result } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js index 4c9a0ce..397804a 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js @@ -23,26 +23,217 @@ class ExpressionBuilder { - constructor(tokenizer) { + constructor(tokenizer, rememberTokens = false) { this.tokenizer = tokenizer; + if(tokenizer.tokenizeWhitespaces) { + console.warn('WARNING: The ExpressionTokenizer for ExpressionBuilder was initialized with whitespace support. Disabled.') + tokenizer.tokenizeWhitespaces = false + } + if(tokenizer.differentiateIdentifiers) { + console.warn('WARNING: The ExpressionTokenizer for ExpressionBuilder was initialized with identifier differentiation support. Disabled.') + tokenizer.differentiateIdentifiers = false + } + this.tokens = [] + this.rememberTokens = rememberTokens this.stack = [] } - parseExpression(delimitors = '') { - // Parse a sequence of operations, and orders them based on OPERATION_PRIORITY. - let elements = [] - let operators = [] - let firstToken = this.tokenizer.peek(); - if(firstToken.type == TK.TokenType.OPERATOR) // First operations. - if(firstToken.value == "-") { - // TODO: Set initial argument. - this.tokenizer.skip(TK.TokenType.OPERATOR) - } else - tokenizer.input.raise(`Invalid operator ${firstToken.value} at begining of statement.`) - else { - - } + /** + * Parses an expression until the end is reached. + * + * @throws {Error} When an unexpected token is encountered. + * @returns {AbstractSyntaxElement} + */ + parseFullExpression() { + this.parseExpression([]) + return this.stack.pop() } - parseOperation()` + /** + * Parses an expression until the end is reached. + * + * @param {string} punctuationDelimitators - List of deliminators that ends the expression + * @throws {Error} When an unexpected token is encountered. + */ + parseExpression(punctuationDelimitators = []) { + let token + while((token = this.tokenizer.peek()) != null) { + if(token.type == TK.TokenType.PUNCT && token.value != '(') // Still allow expression creation. + if(punctuationDelimitators.includes(token.value)) + break + else if(punctuationDelimitators.length == 1) + this.tokenizer.raise(`Unexpected ${token.value}. Expected '${punctuationDelimitators[0]}'.`) + else + this.tokenizer.raise(`Unexpected ${token.value}. Expected one of: '${punctuationDelimitators[0].join("', '")}'.`) + else + this.handleSingle() + } + + if(token == null && punctuationDelimitators.length > 0) + if(punctuationDelimitators.length == 1) + this.tokenizer.raise(`Unexpected end of expression. Expected '${punctuationDelimitators[0]}'.`) + else + this.tokenizer.raise(`Unexpected end of expression. Expected one of: '${punctuationDelimitators[0].join("', '")}'.`) + + + if(this.stack.length == 0) + if(token == null) + this.tokenizer.raise(`Unexpected end of expression. Expected at least one element.`) + else + this.tokenizer.raise(`Unexpected ${token.value}. Expected at least one element.`) + + if(this.stack.length > 1) + this.tokenizer.raise('Invalid expression.') + } + + /** + * Handles a single (assumed non-null) token based on its type. + * + * @param {AbstractSyntaxElement} token + * @throws {Error} When an unexpected token is encountered. + */ + handleSingle(token) { + switch(token.type) { + case TK.TokenType.IDENTIFIER: + this.parseIdentifier() + break + case TK.TokenType.OPERATOR: + if(this.stack.length == 0 && Reference.UNARY_OPERATORS.includes(token.value)) + this.parseSingleOperation() + else if(this.stack.length > 0 && Reference.BINARY_OPERATORS.includes(token.value)) + this.parseBinaryOperations() + else if(this.stack.length > 0 && Reference.TERTIARY_OPERATORS.includes(token.value)) + this.parseTertiaryOperation() + break + case TK.TokenType.NUMBER: + this.stack.push(new AST.NumberElement(this.tokenizer.next().value)) + break + case TK.TokenType.STRING: + this.stack.push(new AST.StringElement(this.tokenizer.next().value)) + break + case TK.TokenType.PUNCT: + if(token.value == '(') { + this.tokenizer.skip(TK.TokenType.PUNCT, '(') // Skip the opening parentheses. + this.parseExpression([')']) + } else + this.tokenizer.raise(`Unexpected ${token.value}. Expected a value.`) + break + default: + this.tokenizer.raise(`Unknown token provided: ${token.value}.`) + break + } + if(this.rememberTokens) + this.tokens.push(token) + } + + /** + * Parses a single token element. + * + * @throws {Error} When an unexpected token is encountered. + */ + parseSingle() { + let token = this.tokenizer.peek() + if(token != null) + this.handleSingle(token) + } + + parseIdentifier() { + // Assuming the right type. + let token = this.tokenizer.read(TK.TokenType.IDENTIFIER) + if(Reference.CONSTANTS_LIST.includes(token.value)) + this.stack.push(new AST.Constant(token.value)) + else + this.stack.push(new AST.Variable(token.value)) + this.checkIdentifierFollowupTokens() + } + + /** + * Parses a function based on a previously called identifier. + * NOTE: Expects to have at least one stack element for function name. + */ + parseFunction() { + // TODO: Implement dynamic functions values instead of being based on names. + let functionValue = this.stack.pop() + if(!(functionValue instanceof AST.Variable)) + this.tokenizer.raise("Executing functions from dynamic variables is not implemented".) + let functionName = functionValue.variableName + let args = [] + let token + while((token = this.tokenizer.peek()) != null && token.value != ')') { + this.tokenizer.skip(TK.TokenType.PUNCT) // Skip the opening parenthesis and the commas. + parseExpression([',',')']) + args.push(this.stack.pop()) + } + + if(token == null) + this.tokenizer.raise(`Unexpected end of expression. Expected ')'.`) + + if(this.functionName == 'derivation') + this.stack.push(new AST.DerivationElement(args)) + else if(this.functionName == 'integral') + this.stack.push(new AST.IntegralElement(args)) + else + this.stack.push(new AST.FunctionElement(functionName, args)) + } + + + /** + * Parses an object property based on a previously called identifier. + * NOTE: Expects to have at least one stack element for property object name. + */ + parseProperty() { + this.tokenizer.skip(TK.TokenType.PUNCT, '.') // Skipping the dot. + let token = this.tokenizer.read(TK.TokenType.IDENTIFIER) + this.stack.push(new AST.PropertyElement(this.stack.pop(), token.value)) + this.checkIdentifierFollowupTokens() + } + + /** + * Parses the value of the element of an array at a given index based on a previously called identifier. + * NOTE: Expects to have at least one stack element for property object name. + */ + parseArrayValue() { + this.tokenizer.skip(TK.TokenType.PUNCT, '[') // Skipping the array opener. + let obj = this.stack.pop() + parseExpression([']']) + this.stack.push(new AST.ArrayElement(obj, this.stack.pop())) + this.checkIdentifierFollowupTokens() + } + + /** + * Checks for followup tokens following a value getting. + * E.g: getting the property of an object, an array member, or calling a function. + * NOTE: Expects to have at least one stack element for previous calling object. + */ + checkIdentifierFollowupTokens() { + let peeked = this.tokenizer.peek() + if(peeked != null && peeked.type == TK.TokenType.PUNCT) + switch(peeked.value) { + case '(': + // Function call + this.parseFunction() + break + case '.': + // Member property + this.parseProperty() + break + case '[': + // Array value + this.parseArrayValue() + break + } + } + + parseBinaryOperations() { + if((this.tokenizer.peek().value in AST.BINARY_OPERATION_PRIORITY)) + throw new Error("Current token is not a binary operator.") + if(this.stack.length == 0) + throw new Error(`The operator ${this.tokenizer.peek().value} can only be used after a value.`) + // Parse a sequence of operations, and orders them based on OPERATION_PRIORITY. + let elements = [this.stack.pop()] + let operators = [this.tokenizer.next()] + let token + while((token = this.tokenizer.peek()) != null) { + } + } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js index e92bc08..ab3fc8a 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js @@ -28,6 +28,21 @@ const CONSTANTS = { }; const CONSTANTS_LIST = Object.keys(CONSTANTS); +const UNARY_OPERATORS = [] + +const BINARY_OPERATION_PRIORITY = { + 'and': 0, 'or': 0, 'in': 0, + '==': 10, '!=' : 10, + '>': 10, '<': 10, '>=': 10, '<=': 10, + '%': 20, '||': 20, + '+': 30, '-': 30, + '*': 40, '/': 40, + '^': 50 +} +const BINARY_OPERATORS = Object.keys(BINARY_OPERATION_PRIORITY) + +const TERTIARY_OPERATORS = ['?'] + const FUNCTIONS = { "abs": Math.abs, "acos": Math.acos, diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js index 63358c0..e49a171 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/tokenizer.js @@ -23,13 +23,14 @@ const WHITESPACES = " \t\n\r" const STRING_LIMITORS = '"\'`'; const OPERATORS = "+-*/^%?:=!><"; -const PUNCTUTATION = "()[]{},."; +const PUNCTUTATION = "()[],."; const NUMBER_CHARS = "0123456789" const IDENTIFIER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_₀₁₂₃₄₅₆₇₈₉αβγδεζηθκλμξρςστφχψωₐₑₒₓₔₕₖₗₘₙₚₛₜ" var TokenType = { // Expression type "WHITESPACE": "WHITESPACE", + "IDENTIFIER": "IDENTIFIER", "VARIABLE": "VARIABLE", "CONSTANT": "CONSTANT", "FUNCTION": "FUNCTION", @@ -49,10 +50,11 @@ class Token { } class ExpressionTokenizer { - constructor(input, tokenizeWhitespaces = false, errorOnUnknown = true) { - this.input = input; - this.currentToken = null; + constructor(input, tokenizeWhitespaces = false, differentiateIdentifiers = false, errorOnUnknown = true) { + this.input = input + this.currentToken = null this.tokenizeWhitespaces = tokenizeWhitespaces + this.differentiateIdentifiers = differentiateIdentifiers this.errorOnUnknown = errorOnUnknown } @@ -115,10 +117,16 @@ class ExpressionTokenizer { while(!this.input.atEnd() && IDENTIFIER_CHARS.includes(this.input.peek().toLowerCase())) { identifier += this.input.next(); } - if(Reference.CONSTANTS_LIST.includes(identifier.toLowerCase())) { - return new Token(TokenType.CONSTANT, identifier.toLowerCase(), this.input.position-identifier.length) - } else if(Reference.FUNCTIONS_LIST.includes(identifier.toLowerCase())) { - return new Token(TokenType.FUNCTION, identifier.toLowerCase(), this.input.position-identifier.length) + let identifierLC = identifier.toLowerCase() + if(Reference.CONSTANTS_LIST.includes(identifierLC)) { + return new Token(TokenType.CONSTANT, identifierLC, this.input.position-identifier.length) + } else if(Reference.FUNCTIONS_LIST.includes(identifierLC)) { + return new Token(TokenType.FUNCTION, identifierLC, this.input.position-identifier.length) + } else if(Reference.UNARY_OPERATORS.includes(identifierLC) || + Reference.BINARY_OPERATORS.includes(identifierLC) || + Reference.TERTIARY_OPERATORS.includes(identifierLC) + ) { + return new Token(TokenType.OPERATOR, identifierLC, this.input.position-identifier.length) } else { return new Token(TokenType.VARIABLE, identifier, this.input.position-identifier.length) } @@ -137,7 +145,7 @@ class ExpressionTokenizer { if(Reference.CONSTANTS_LIST.includes(c)) return new Token(TokenType.CONSTANT, this.input.next(), this.input.position-1); if(PUNCTUTATION.includes(c)) return new Token(TokenType.PUNCT, this.input.next(), this.input.position-1); if(this.errorOnUnknown) - this.input.throw("Unknown token character " + c) + this.raise("Unknown token character " + c) else return new Token(TokenType.UNKNOWN, this.input.next(), this.input.position-1); } @@ -156,14 +164,25 @@ class ExpressionTokenizer { this.currentToken = null; return tmp; } + + read(type, value) { + let next = this.next() + if(type != null && next.type != type) + this.raise(`Unexpected ${next.type.toLowerCase()} ${next.value}. Expected type was ${type.toLowerCase()}.`); + if(value != null && next.value == value) + this.raise(`Unexpected ${next.type.toLowerCase()} ${next.value}. Expected value was ${value}.`); + return next + } atEnd() { return this.peek() == null; } - skip(type) { - let next = this.next(); - if(next.type != type) - input.raise("Unexpected token " + next.type.toLowerCase() + ' "' + next.value + '". Expected ' + type.toLowerCase()); + skip(type, value) { + this.read(type, value) + } + + raise(message) { + this.input.raise(message) } } From 80d0dad63a2232f8c089cef21d990552f2f92f94 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Sun, 23 Oct 2022 18:09:20 +0200 Subject: [PATCH 3/4] Implementing parseBinaryOperator --- .../ad5001/LogarithmPlotter/js/parsing/ast.js | 51 ++-------------- .../LogarithmPlotter/js/parsing/builder.js | 61 ++++++++++++++++--- .../LogarithmPlotter/js/parsing/reference.js | 4 +- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js index 160ebfb..8c5f00b 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js @@ -614,6 +614,9 @@ class IntegralElement extends FunctionElement { } } + +class UnaryOperation extends AbstractSyntaxElement {} + class BinaryOperation extends AbstractSyntaxElement { type = ASEType.BINARY_OPERATION @@ -884,49 +887,5 @@ class Negation extends AbstractSyntaxElement { } } -class Negation extends AbstractSyntaxElement { - type = ASEType.NEGATION - - constructor(expression) { - this.expression = expression - } - - execute(variables) { - if(variables.includes(this.arrayName)) { - let index = this.astIndex.execute(variables) - if(index % 1 != 0 || index < 0) { // Float index. - throw new EvalError("Non-integer array index " + index + " used as array index for " + this.variableName + ".") - } else if(variables[this.arrayName].length <= index) { - throw new EvalError("Out-of-range index " + index + " used as array index for " + this.variableName + ".") - } else { - return variables[this.arrayName][index] - } - } else { - throw new EvalError("Unknown variable " + this.variableName + ".") - } - - toLatex() { - return this.variableName - } - } - - simplify() { - return new Negation(this.expression.simplify()) - } - - derivation(variable) { - return new Negation(this.expression.derivation(variable)) - } - - integral(variable) { - return new Negation(this.expression.integral(variable)) - } - - toLatex() { - return '-' + this.expression.toLatex() - } - - isConstant() { - return this.expression.isConstant() - } -} + +class TertiaryOperation extends AbstractSyntaxElement {} diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js index 397804a..8ca9e0f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/builder.js @@ -94,9 +94,13 @@ class ExpressionBuilder { */ handleSingle(token) { switch(token.type) { - case TK.TokenType.IDENTIFIER: - this.parseIdentifier() + case TK.TokenType.NUMBER: + this.stack.push(new AST.NumberElement(this.tokenizer.next().value)) break + case TK.TokenType.STRING: + this.stack.push(new AST.StringElement(this.tokenizer.next().value)) + break + case TK.TokenType.IDENTIFIER: case TK.TokenType.OPERATOR: if(this.stack.length == 0 && Reference.UNARY_OPERATORS.includes(token.value)) this.parseSingleOperation() @@ -104,12 +108,11 @@ class ExpressionBuilder { this.parseBinaryOperations() else if(this.stack.length > 0 && Reference.TERTIARY_OPERATORS.includes(token.value)) this.parseTertiaryOperation() - break - case TK.TokenType.NUMBER: - this.stack.push(new AST.NumberElement(this.tokenizer.next().value)) - break - case TK.TokenType.STRING: - this.stack.push(new AST.StringElement(this.tokenizer.next().value)) + else if(token.type == TK.TokenType.IDENTIFIER) + // If it isn't a reserved keyword for operators (e.g and, or...), then it *is* and identifier. + this.parseIdentifier() + else + this.tokenizer.raise(`Unknown operator: ${token.value}.`) break case TK.TokenType.PUNCT: if(token.value == '(') { @@ -202,7 +205,7 @@ class ExpressionBuilder { /** * Checks for followup tokens following a value getting. - * E.g: getting the property of an object, an array member, or calling a function. + * E.g: getting the property of an object, an array member, or calling a function.^ * NOTE: Expects to have at least one stack element for previous calling object. */ checkIdentifierFollowupTokens() { @@ -231,9 +234,47 @@ class ExpressionBuilder { throw new Error(`The operator ${this.tokenizer.peek().value} can only be used after a value.`) // Parse a sequence of operations, and orders them based on OPERATION_PRIORITY. let elements = [this.stack.pop()] - let operators = [this.tokenizer.next()] + let operators = [this.tokenizer.next().value] + let nextIsOperator = false let token while((token = this.tokenizer.peek()) != null) { + if(nextIsOperator) + if(token.type == TK.TokenType.PUNCT) + if(token.value == ')') + // Don't skip that token, but stop the parsing, + // as it may be an unopened expression. + break + else + this.tokenizer.raise(`Unexpected ${token.value}. Expected an operator, or ')'.`) + else if(token.type == TK.TokenType.IDENTIFIER) + if(Reference.BINARY_OPERATORS.includes(token.value)) + this.operartors.push(this.tokenizer.next().value) + else if(Reference.TERTIARY_OPERATORS.includes(token.value)) + // Break to let the hand back to the parser. + break + else if(Reference.UNARY_OPERATORS.includes(token.value)) + this.tokenizer.raise(`Invalid use of operator ${token.value} after ${elements.pop().value}.`) + else + this.tokenizer.raise(`Unknown operator: ${token.value}.`) + else { + handleSingle(token) + let value = this.stack.pop() + if(token.value != '(' && (value instanceof AST.BinaryOperation || value instanceof AST.TertiaryOperation)) + // In case you chain something like 'or' and '*' + // Unary operations are exempted from this as they are used for a single value. + this.tokenizer.raise(`Cannot chain operations ${operators.pop().value} and ${value.ope}.`) + elements.push(value) + } } + // Now we have our full chain, we need to match by operation priority + // TODO: Implement FlatBinaryOperations for better simplification and smarter trees. + for(let ope of AST.BINARY_OPERATORS) + while(operators.includes(ope)) { // Skip if not in priority. + let index = operators.indexOf(ope) + operators.splice(index, 1) // Remove operator from array. + elements.splice(index, 2, new BinaryOperation(elements[index], ope, elements[index+1])) + } + // At the end, there should be no more operators and only one element. + this.stack.push(elements.pop()) } } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js index ab3fc8a..c78e4b9 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/reference.js @@ -39,7 +39,9 @@ const BINARY_OPERATION_PRIORITY = { '*': 40, '/': 40, '^': 50 } -const BINARY_OPERATORS = Object.keys(BINARY_OPERATION_PRIORITY) + +// Sorted by priority (most to least) +const BINARY_OPERATORS = Object.keys(BINARY_OPERATION_PRIORITY).sort((ope1, ope2) => BINARY_OPERATION_PRIORITY[ope2]-BINARY_OPERATION_PRIORITY[ope1]) const TERTIARY_OPERATORS = ['?'] From 64d5f11ff9cee2199b9a4c0ae53d1d2b39699879 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 25 Oct 2022 16:26:08 +0200 Subject: [PATCH 4/4] FlatPlusOperation for future simplifications! --- .../ad5001/LogarithmPlotter/js/parsing/ast.js | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js index 8c5f00b..850c952 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js @@ -38,6 +38,7 @@ enum ASEType { UNARY_OPERATION, BINARY_OPERATION, TERTIARY_OPERATION, + FLAT_PLUS_OPERATIONS } /** @@ -826,6 +827,170 @@ class BinaryOperation extends AbstractSyntaxElement { } } +/** + * This class is used to contain chains of + and - operations for simplification purposes + */ +class FlatPlusOperations extends AbstractSyntaxElement { + type = ASEType.FLAT_PLUS_OPERATIONS + + constructor(elements, operators) { + if(this.operators.some(ope => ope != '+' && ope != '-')) + throw new Error(`Unallowed operators in flat plus operation: ${this.operators.filter(ope => ope != '+' && ope != '-').join(',')}.`) + if(this.operators.length != this.elements.length+1) + throw new Error(`Unmatched elements and error count.`) + this.elements = elements + this.operators = operators + } + + + evaluate(variables) { + let result = this.elements[0].evaluate(variables); + for(let i = 0; i < this.operators.length; i++) + if(this.operators[i] == '+') + result += this.elements[i+1].evaluate(variables) + else + result -= this.elements[i+1].evaluate(variables) + return result + } + + simplify() { + let elements = this.elements.map(elem => elem.simplify()) + let units = 0 + let variablesNoms = {} // Polynom elements for each variable. + let others = {'-': [], '+': []} + let nome + for(let i = 0; i < elements.length; i++) + if(elements[i].isConstant()) + if(i == 0 || this.operators[i-1] == '+') + units += elements[i].evaluate({}) + else + units -= elements[i].evaluate({}) + else if((nome = isNome(elements[i]))[0]) { + let [nom, variable, power] = nome + if(!(variable in variablesNoms)) + variablesNoms[variable] = {} + if(variablesNoms[variable][power] == undefined) + variablesNoms[variable][power] = 0 + if(i == 0 || this.operators[i-1] == '+') + variablesNoms[variable][power]++ + else + variablesNoms[varName][power]-- + } else if(elements[i] instanceof BinaryOperation && elements[i].ope == '*' && + elements[i].leftHand.isConstant() && (nome = isNome(elements[i].rightHand instanceof Variable))[0]) { + // Simple constant by variable product + let number = elements[i].leftHand.evaluate({}) + let [nom, variable, power] = nome + // Add to counter + if(!(variable in variablesNoms)) + variablesNoms[variable] = {} + if(variablesNoms[variable][power] == undefined) + variablesNoms[variable][power] = 0 + if(i == 0 || this.operators[i-1] == '+') + variablesNoms[variable][power] += number + else + variablesNoms[variable][power] -= number + } else if(elements[i] instanceof BinaryOperation && elements[i].ope == '*' && + (nome = isNome(elements[i].leftHand))[0] && elements[i].rightHand.isConstant()) { + // Simple constant by variable product + let number = elements[i].rightHand.evaluate({}) + let [nom, variable, power] = nome + // Add to counter + if(!(variable in variablesNoms)) + variablesNoms[variable] = {} + if(variablesNoms[variable][power] == undefined) + variablesNoms[variable][power] = 0 + if(i == 0 || this.operators[i-1] == '+') + variablesNoms[variable][power] += number + else + variablesNoms[variable][power] -= number + } else if(elements[i] instanceof BinaryOperation && elements[i].ope == '/' && + (nome = isNome(elements[i].leftHand))[0] && elements[i].rightHand.isConstant()) { + // Simple constant by variable divided by constant + let number = elements[i].rightHand.evaluate({}) + let [nom, variable, power] = nome + // Register in counter + if(!(variable in variablesNoms)) + variablesNoms[variable] = {} + if(variablesNoms[variable][power] == undefined) + variablesNoms[variable][power] = 0 + // Division so we remove from count + if(i == 0 || this.operators[i-1] == '+') + variablesNoms[variable][power] -= number.evaluate({}) + else + variablesNoms[variable][power] += number.evaluate({}) + } else { + let sign = this.operators[i-1] + let element = elements[i] + if(element instanceof UnaryOperation && element.ope == '-') { + // Negation, inverse signs + sign = sign == '+' ? '-' : '+' + element = element.element + } + others[sign] = element + } + // Creating new flat operation. + let newElements = [new NumberElement(units)] // Yes, I know units should be in last, but this simplifies the implementation a lot. + let newOperators = [] + for(let variable in variablesNoms) + for(let power in Object.keys(variablesNoms[variable]).reverse()) { + power = parseFloat(power) + let times = variablesNoms[variable][nom] + if(power < 0) { + power = -power + newOperators.push('-') + } else + newOperators.push('+') + newElements.push(new Variable(variable)) + if(power != 1) + newElements.push(new BinaryOperation(newElements.pop(), '^', new NumberElement(power))) + if(times != 1) + newElements.push(new BinaryOperation(new NumberElement(times), '*', newElements.pop())) + } + for(let sign in others) + for(let element of others[sign]) { + newOperators.push(sign) + newElements.push(element) + } + return new FlatPlusOperations(newElements, newOperators) + } + + derivation(variable) { + return new FlatPlusOperations(this.elements.map(elem => elem.derivation(variable)), this.operators) + } + + integral(variable) { + return new FlatPlusOperations(this.elements.map(elem => elem.integral(variable)), this.operators) + } + + toEditableString() { + let ret = this.elements[0].toEditableString() + for(let i = 0; i < this.operators.length; i++) { + ret += ' ' + this.operators[i] + ' ' + if(this.elements[i+1] instanceof BinaryOperation && Reference.BINARY_OPERATION_PRIORITY[this.elements[i+1].ope] < Reference.BINARY_OPERATION_PRIORITY['+']) + ret += `(${this.elements[i+1].toEditableString()})` + else + ret += this.elements[i+1].toEditableString() + } + return ret + } + + toLatex() { + let ret = this.elements[0].toLatex() + for(let i = 0; i < this.operators.length; i++) { + ret += ' ' + this.operators[i] + ' ' + if(this.elements[i+1] instanceof BinaryOperation && Reference.BINARY_OPERATION_PRIORITY[this.elements[i+1].ope] < Reference.BINARY_OPERATION_PRIORITY['+']) + ret += `\\left(${this.elements[i+1].toLatex()}\\right)` + else + ret += this.elements[i+1].toLatex() + } + return ret + } + + isConstant() { + return this.elements.every(elem => elem.isConstant()) + } +} + function simplifyFraction(num,den) { // More than gcd because it allows decimals fractions. let mult = 1 @@ -846,6 +1011,17 @@ function simplifyFraction(num,den) { return [num*mult/gcd, den*mult/gcd] } +function isNome(element) { + // Checks if the elements is a part of a polynomial, which variable, and to which degree + if(element instanceof Variable) + return [true, element.variableName, 1] + else if(element instanceof BinaryOperation && element.ope == '^' && element.leftHand instanceof Variable && element.rightHand.isConstant()) + return [true, element.leftHand.variableName, element.rightHand.evaluate({})] + else + return [false, '', 0] +} + + class Negation extends AbstractSyntaxElement { type = ASEType.NEGATION