Adding new expression tests.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Adsooi 2024-10-26 01:06:24 +02:00
parent a01b7a17ef
commit 2594fd6844
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
9 changed files with 274 additions and 62 deletions

View file

@ -95,8 +95,8 @@ Finally, to actually run the tests:
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
Language files translations located at LogarithmPlotter/i18n are licensed under GNU GPL3.0+ and are copyrighted by their See LICENSE.md for more details. Language files translations located at assets/i18n are licensed under GNU GPL3.0+ and
original authors. See LICENSE.md for more details: are copyrighted by their original authors:
- 🇭🇺 Hungarian translation by [Óvári](https://github.com/ovari) - 🇭🇺 Hungarian translation by [Óvári](https://github.com/ovari)
- 🇳🇴 Norwegian translation by [Allan Nordhøy](https://github.com/comradekingu) - 🇳🇴 Norwegian translation by [Allan Nordhøy](https://github.com/comradekingu)
@ -109,5 +109,5 @@ of [ndef.parser](https://web.archive.org/web/20111023001618/http://www.undefined
&lt;r@undefined.ch&gt;, ported to javascript by Matthew Crumley &lt;r@undefined.ch&gt;, ported to javascript by Matthew Crumley
&lt;email@matthewcrumley.com&gt; (http://silentmatt.com/), and then to QMLJS by Ad5001. &lt;email@matthewcrumley.com&gt; (http://silentmatt.com/), and then to QMLJS by Ad5001.
All files in (LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/lib/expr-eval/) except integration.mjs are licensed All files in (common/src/lib/expr-eval/) except integration.mjs are licensed
under the [MIT License](https://raw.githubusercontent.com/silentmatt/expr-eval/master/LICENSE.txt). under the [MIT License](https://raw.githubusercontent.com/silentmatt/expr-eval/master/LICENSE.txt).

View file

@ -21,24 +21,34 @@ import * as Utils from "../utils.mjs"
import Latex from "../module/latex.mjs" import Latex from "../module/latex.mjs"
import ExprParser from "../module/expreval.mjs" import ExprParser from "../module/expreval.mjs"
import Objects from "../module/objects.mjs" import Objects from "../module/objects.mjs"
import { ExprEvalExpression } from "../lib/expr-eval/expression.mjs"
const NUMBER_MATCHER = /^\d*\.\d+(e[+-]\d+)?$/
/** /**
* Represents any kind of x-based or non variable based expression. * Represents any kind of x-based or non variable based expression.
*/ */
export class Expression { export class Expression {
/**
*
* @param {string|ExprEvalExpression} expr
*/
constructor(expr) { constructor(expr) {
if(typeof expr === "string") { if(typeof expr === "string") {
this.expr = Utils.exponentsToExpression(expr) this.expr = Utils.exponentsToExpression(expr)
this.calc = ExprParser.parse(this.expr).simplify() this.calc = ExprParser.parse(this.expr).simplify()
} else { } else if(expr instanceof ExprEvalExpression) {
// Passed an expression here directly. // Passed an expression here directly.
this.calc = expr.simplify() this.calc = expr.simplify()
this.expr = expr.toString() this.expr = expr.toString()
} else {
const type = expr != null ? "a " + expr.constructor.name : expr
throw new Error(`Cannot create an expression with ${type}.`)
} }
this.cached = this.isConstant() this.canBeCached = this.isConstant()
this.cachedValue = null this.cachedValue = null
if(this.cached && this.allRequirementsFulfilled()) if(this.canBeCached && this.allRequirementsFulfilled())
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName) this.recache()
this.latexMarkup = Latex.expression(this.calc.tokens) this.latexMarkup = Latex.expression(this.calc.tokens)
} }
@ -77,21 +87,20 @@ export class Expression {
/** /**
* Returns a list of names whose corresponding objects this expression is dependant on and are missing. * Returns a list of names whose corresponding objects this expression is dependant on and are missing.
* @return {boolean} * @return {string[]}
*/ */
undefinedVariables() { undefinedVariables() {
return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName)) return this.requiredObjects().filter(objName => !(objName in Objects.currentObjectsByName))
} }
recache() { recache() {
if(this.cached) this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName)
} }
execute(x = 1) { execute(x = 1) {
if(this.cached) { if(this.canBeCached) {
if(this.cachedValue == null) if(this.cachedValue == null)
this.cachedValue = this.calc.evaluate(Objects.currentObjectsByName) this.recache()
return this.cachedValue return this.cachedValue
} }
ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName) ExprParser.currentVars = Object.assign({ "x": x }, Objects.currentObjectsByName)
@ -99,9 +108,10 @@ export class Expression {
} }
simplify(x) { simplify(x) {
let expr = this.calc.substitute("x", x).simplify() let expr = new Expression(this.calc.substitute("x", x).simplify())
if(expr.evaluate() === 0) expr = "0" if(expr.allRequirementsFulfilled() && expr.execute() === 0)
return new Expression(expr) expr = new Expression("0")
return expr
} }
toEditableString() { toEditableString() {
@ -110,17 +120,28 @@ export class Expression {
toString(forceSign = false) { toString(forceSign = false) {
let str = Utils.makeExpressionReadable(this.calc.toString()) let str = Utils.makeExpressionReadable(this.calc.toString())
if(str !== undefined && str.match(/^\d*\.\d+$/)) { if(str !== undefined && str.match(NUMBER_MATCHER)) {
if(str.split(".")[1].split("0").length > 7) { const decimals = str.split(".")[1].split("e")[0]
const zeros = decimals.split("0").length
const nines = decimals.split("9").length
if(zeros > 7 || nines > 7) {
// Likely rounding error // Likely rounding error
str = parseFloat(str.substring(0, str.length - 1)).toString() str = parseFloat(str).toDecimalPrecision(8).toString()
} }
} }
if(str[0] !== "-" && forceSign) str = "+" + str if(str[0] === "(" && str.at(-1) === ")")
str = str.substring(1, str.length - 1)
if(str[0] !== "-" && forceSign)
str = "+" + str
return str return str
} }
} }
/**
* Parses and executes the given expression
* @param {string} expr
* @return {number}
*/
export function executeExpression(expr) { export function executeExpression(expr) {
return (new Expression(expr.toString())).execute() return (new Expression(expr.toString())).execute()
} }

View file

@ -19,7 +19,6 @@
import { Module } from "./common.mjs" import { Module } from "./common.mjs"
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import History from "./history.mjs" import History from "./history.mjs"
import Canvas from "./canvas.mjs"
import Settings from "./settings.mjs" import Settings from "./settings.mjs"
import { DialogInterface, RootInterface } from "./interface.mjs" import { DialogInterface, RootInterface } from "./interface.mjs"
import { BaseEvent } from "../events.mjs" import { BaseEvent } from "../events.mjs"

View file

@ -224,7 +224,7 @@ export class DrawableObject {
currentObjectsByName[objName].requiredBy.push(this) currentObjectsByName[objName].requiredBy.push(this)
} }
} }
if(this[property].cached && this[property].requiredObjects().length > 0) if(this[property].canBeCached && this[property].requiredObjects().length > 0)
// Recalculate // Recalculate
this[property].recache() this[property].recache()

View file

@ -35,50 +35,50 @@ import {
} from "../../src/lib/expr-eval/polyfill.mjs" } from "../../src/lib/expr-eval/polyfill.mjs"
describe("Math/Polyfill", () => { describe("Math/Polyfill", () => {
describe("#AADDDD", function() { describe("#add", function() {
it("should add two numbers", function() { it("adds two numbers", function() {
expect(Polyfill.add(2, 3)).to.equal(5) expect(Polyfill.add(2, 3)).to.equal(5)
expect(Polyfill.add("2", "3")).to.equal(5) expect(Polyfill.add("2", "3")).to.equal(5)
}) })
}) })
describe("#sub", function() { describe("#sub", function() {
it("should subtract two numbers", function() { it("subtracts two numbers", function() {
expect(Polyfill.sub(2, 1)).to.equal(1) expect(Polyfill.sub(2, 1)).to.equal(1)
expect(Polyfill.sub("2", "1")).to.equal(1) expect(Polyfill.sub("2", "1")).to.equal(1)
}) })
}) })
describe("#mul", function() { describe("#mul", function() {
it("should multiply two numbers", function() { it("multiplies two numbers", function() {
expect(Polyfill.mul(2, 3)).to.equal(6) expect(Polyfill.mul(2, 3)).to.equal(6)
expect(Polyfill.mul("2", "3")).to.equal(6) expect(Polyfill.mul("2", "3")).to.equal(6)
}) })
}) })
describe("#div", function() { describe("#div", function() {
it("should divide two numbers", function() { it("divides two numbers", function() {
expect(Polyfill.div(10, 2)).to.equal(5) expect(Polyfill.div(10, 2)).to.equal(5)
expect(Polyfill.div("10", "2")).to.equal(5) expect(Polyfill.div("10", "2")).to.equal(5)
}) })
}) })
describe("#mod", function() { describe("#mod", function() {
it("should return the modulo of two numbers", function() { it("returns the modulo of two numbers", function() {
expect(Polyfill.mod(10, 3)).to.equal(1) expect(Polyfill.mod(10, 3)).to.equal(1)
expect(Polyfill.mod("10", "3")).to.equal(1) expect(Polyfill.mod("10", "3")).to.equal(1)
}) })
}) })
describe("#concat", function() { describe("#concat", function() {
it("should return the concatenation of two strings", function() { it("returns the concatenation of two strings", function() {
expect(Polyfill.concat(10, 3)).to.equal("103") expect(Polyfill.concat(10, 3)).to.equal("103")
expect(Polyfill.concat("abc", "def")).to.equal("abcdef") expect(Polyfill.concat("abc", "def")).to.equal("abcdef")
}) })
}) })
describe("#equal", function() { describe("#equal", function() {
it("should return whether its two arguments are equal", function() { it("returns whether its two arguments are equal", function() {
expect(Polyfill.equal(10, 3)).to.be.false expect(Polyfill.equal(10, 3)).to.be.false
expect(Polyfill.equal(10, 10)).to.be.true expect(Polyfill.equal(10, 10)).to.be.true
expect(Polyfill.equal("abc", "def")).to.be.false expect(Polyfill.equal("abc", "def")).to.be.false
@ -87,7 +87,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#notEqual", function() { describe("#notEqual", function() {
it("should return whether its two arguments are not equal", function() { it("returns whether its two arguments are not equal", function() {
expect(Polyfill.notEqual(10, 3)).to.be.true expect(Polyfill.notEqual(10, 3)).to.be.true
expect(Polyfill.notEqual(10, 10)).to.be.false expect(Polyfill.notEqual(10, 10)).to.be.false
expect(Polyfill.notEqual("abc", "def")).to.be.true expect(Polyfill.notEqual("abc", "def")).to.be.true
@ -96,7 +96,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#greaterThan", function() { describe("#greaterThan", function() {
it("should return whether its first argument is strictly greater than its second", function() { it("returns whether its first argument is strictly greater than its second", function() {
expect(Polyfill.greaterThan(10, 3)).to.be.true expect(Polyfill.greaterThan(10, 3)).to.be.true
expect(Polyfill.greaterThan(10, 10)).to.be.false expect(Polyfill.greaterThan(10, 10)).to.be.false
expect(Polyfill.greaterThan(10, 30)).to.be.false expect(Polyfill.greaterThan(10, 30)).to.be.false
@ -104,7 +104,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#lessThan", function() { describe("#lessThan", function() {
it("should return whether its first argument is strictly less than its second", function() { it("returns whether its first argument is strictly less than its second", function() {
expect(Polyfill.lessThan(10, 3)).to.be.false expect(Polyfill.lessThan(10, 3)).to.be.false
expect(Polyfill.lessThan(10, 10)).to.be.false expect(Polyfill.lessThan(10, 10)).to.be.false
expect(Polyfill.lessThan(10, 30)).to.be.true expect(Polyfill.lessThan(10, 30)).to.be.true
@ -112,7 +112,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#greaterThanEqual", function() { describe("#greaterThanEqual", function() {
it("should return whether its first argument is greater or equal to its second", function() { it("returns whether its first argument is greater or equal to its second", function() {
expect(Polyfill.greaterThanEqual(10, 3)).to.be.true expect(Polyfill.greaterThanEqual(10, 3)).to.be.true
expect(Polyfill.greaterThanEqual(10, 10)).to.be.true expect(Polyfill.greaterThanEqual(10, 10)).to.be.true
expect(Polyfill.greaterThanEqual(10, 30)).to.be.false expect(Polyfill.greaterThanEqual(10, 30)).to.be.false
@ -120,7 +120,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#lessThanEqual", function() { describe("#lessThanEqual", function() {
it("should return whether its first argument is strictly less than its second", function() { it("returns whether its first argument is strictly less than its second", function() {
expect(Polyfill.lessThanEqual(10, 3)).to.be.false expect(Polyfill.lessThanEqual(10, 3)).to.be.false
expect(Polyfill.lessThanEqual(10, 10)).to.be.true expect(Polyfill.lessThanEqual(10, 10)).to.be.true
expect(Polyfill.lessThanEqual(10, 30)).to.be.true expect(Polyfill.lessThanEqual(10, 30)).to.be.true
@ -128,7 +128,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#andOperator", function() { describe("#andOperator", function() {
it("should return whether its arguments are both true", function() { it("returns whether its arguments are both true", function() {
expect(Polyfill.andOperator(true, true)).to.be.true expect(Polyfill.andOperator(true, true)).to.be.true
expect(Polyfill.andOperator(true, false)).to.be.false expect(Polyfill.andOperator(true, false)).to.be.false
expect(Polyfill.andOperator(false, true)).to.be.false expect(Polyfill.andOperator(false, true)).to.be.false
@ -140,7 +140,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#orOperator", function() { describe("#orOperator", function() {
it("should return whether one of its arguments is true", function() { it("returns whether one of its arguments is true", function() {
expect(Polyfill.orOperator(true, true)).to.be.true expect(Polyfill.orOperator(true, true)).to.be.true
expect(Polyfill.orOperator(true, false)).to.be.true expect(Polyfill.orOperator(true, false)).to.be.true
expect(Polyfill.orOperator(false, true)).to.be.true expect(Polyfill.orOperator(false, true)).to.be.true
@ -152,7 +152,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#inOperator", function() { describe("#inOperator", function() {
it("should check if second argument contains first", function() { it("checks if second argument contains first", function() {
expect(Polyfill.inOperator("a", ["a", "b", "c"])).to.be.true expect(Polyfill.inOperator("a", ["a", "b", "c"])).to.be.true
expect(Polyfill.inOperator(3, [0, 1, 2])).to.be.false expect(Polyfill.inOperator(3, [0, 1, 2])).to.be.false
expect(Polyfill.inOperator(3, [0, 1, 3, 2])).to.be.true expect(Polyfill.inOperator(3, [0, 1, 3, 2])).to.be.true
@ -181,14 +181,14 @@ describe("Math/Polyfill", () => {
}) })
describe("#trunc", function() { describe("#trunc", function() {
it("should return the decimal part of floats", function() { it("returns the decimal part of floats", function() {
for(let x = -10; x < 10; x += 0.1) for(let x = -10; x < 10; x += 0.1)
expect(Polyfill.trunc(x)).to.equal(Math.trunc(x)) expect(Polyfill.trunc(x)).to.equal(Math.trunc(x))
}) })
}) })
describe("#gamma", function() { describe("#gamma", function() {
it("should return the product of factorial(x - 1)", function() { it("returns the product of factorial(x - 1)", function() {
expect(Polyfill.gamma(0)).to.equal(Infinity) expect(Polyfill.gamma(0)).to.equal(Infinity)
expect(Polyfill.gamma(1)).to.equal(1) expect(Polyfill.gamma(1)).to.equal(1)
expect(Polyfill.gamma(2)).to.equal(1) expect(Polyfill.gamma(2)).to.equal(1)
@ -204,7 +204,7 @@ describe("Math/Polyfill", () => {
}) })
describe("#hypot", function() { describe("#hypot", function() {
it("should return the hypothenus length of a triangle whose length are provided in arguments", function() { it("returns the hypothenus length of a triangle whose length are provided in arguments", function() {
for(let x = 0; x < 10; x += 0.3) { for(let x = 0; x < 10; x += 0.3) {
expect(Polyfill.hypot(x)).to.be.approximately(Math.hypot(x), Number.EPSILON) expect(Polyfill.hypot(x)).to.be.approximately(Math.hypot(x), Number.EPSILON)
for(let y = 0; y < 10; y += 0.3) { for(let y = 0; y < 10; y += 0.3) {
@ -224,7 +224,8 @@ describe("Math/Polyfill", () => {
describe("#log1p, #log2", function() { describe("#log1p, #log2", function() {
for(let x = 1; x < 10; x += 0.3) { for(let x = 1; x < 10; x += 0.3) {
expect(Polyfill.log1p(x)).to.be.approximately(Math.log1p(x), 1e-12) expect(Polyfill.log1p(x)).to
.be.approximately(Math.log1p(x), 1e-12)
expect(Polyfill.log2(x)).to.be.approximately(Math.log2(x), 1e-12) expect(Polyfill.log2(x)).to.be.approximately(Math.log2(x), 1e-12)
} }
}) })

View file

@ -0,0 +1,181 @@
/**
* 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/>.
*/
// Load prior tests
import "../basics/utils.mjs"
import "../module/latex.mjs"
import "../module/expreval.mjs"
import "../module/objects.mjs"
import { describe, it } from "mocha"
import { expect } from "chai"
import { executeExpression, Expression } from "../../src/math/expression.mjs"
import ExprEval from "../../src/module/expreval.mjs"
describe("Math/Expression", function() {
describe("#constructor", function() {
it("accepts strings", function() {
expect(() => new Expression("2+3")).to.not.throw
expect(() => new Expression("x+2")).to.not.throw
})
it("accepts already parsed expressions", function() {
expect(() => new Expression(ExprEval.parse("2+3"))).to.not.throw
expect(() => new Expression(ExprEval.parse("x+2"))).to.not.throw
})
it("doesn't accept anything else", function() {
expect(() => new Expression()).to.throw("Cannot create an expression with undefined.")
expect(() => new Expression(12)).to.throw("Cannot create an expression with a Number.")
expect(() => new Expression({})).to.throw("Cannot create an expression with a Object.")
expect(() => new Expression(true)).to.throw("Cannot create an expression with a Boolean.")
})
})
describe("#variables", function() {
it("returns a list of variables for non-constant expressions", function() {
expect(new Expression("x+1").variables()).to.deep.equal(["x"])
expect(new Expression("x+n").variables()).to.deep.equal(["x", "n"])
expect(new Expression("u[n] + A.x").variables()).to.deep.equal(["u", "n", "A"])
})
it("returns an empty array if the expression is constant", function() {
expect(new Expression("2+1").variables()).to.deep.equal([])
expect(new Expression("sin π").variables()).to.deep.equal([])
expect(new Expression("e^3").variables()).to.deep.equal([])
})
})
describe("#isConstant", function() {
it("returns true if neither x nor n are included into the expression", function() {
expect(new Expression("2+1").isConstant()).to.be.true
expect(new Expression("e^3").isConstant()).to.be.true
expect(new Expression("2+f(3)").isConstant()).to.be.true
expect(new Expression("sin A.x").isConstant()).to.be.true
})
it("returns false if either x or n are included into the expression", function() {
expect(new Expression("2+x").isConstant()).to.be.false
expect(new Expression("e^n").isConstant()).to.be.false
expect(new Expression("2+f(x)").isConstant()).to.be.false
expect(new Expression("n + sin x").isConstant()).to.be.false
})
})
describe("#requiredObjects", function() {
it("returns the list of objects that need to be registered for this expression", function() {
expect(new Expression("x^n").requiredObjects()).to.deep.equal([])
expect(new Expression("2+f(3)").requiredObjects()).to.deep.equal(["f"])
expect(new Expression("A.x+x").requiredObjects()).to.deep.equal(["A"])
expect(new Expression("2+f(sin A.x)+n").requiredObjects()).to.deep.equal(["f", "A"])
})
})
describe.skip("#allRequirementsFulfilled", function() {
// TODO: Make tests for objects
})
describe.skip("#undefinedVariables", function() {
// TODO: Make tests for objects
})
describe("#toEditableString", function() {
it("should return a readable expression", function() {
expect(new Expression("2+1").toEditableString()).to.equal("3")
expect(new Expression("2+x").toEditableString()).to.equal("(2 + x)")
expect(new Expression("x*2+x/3").toEditableString()).to.equal("((x * 2) + (x / 3))")
})
it("should be able to be reparsed and equal the same expression", function() {
const exprs = ["5", "x/2", "4/2", "sin x"]
for(const expr of exprs) {
const exprObj = new Expression(expr)
expect(new Expression(exprObj.toEditableString()).calc).to.deep.equal(exprObj.calc)
}
})
})
describe("#execute", function() {
it("returns the result of the computation of the expression", function() {
expect(new Expression("2+3").execute()).to.equal(5)
expect(new Expression("2+3").execute(10)).to.equal(5)
expect(new Expression("2+x").execute(10)).to.equal(12)
expect(new Expression("sin x").execute(Math.PI)).to.be.approximately(0, Number.EPSILON)
})
it("returns the cached value if the expression can be cached", function() {
const exprs = ["2+3", "x/2", "4/2", "sin x"]
for(const expr of exprs) {
const exprObj = new Expression(expr)
if(exprObj.canBeCached)
expect(exprObj.execute()).to.equal(exprObj.cachedValue)
else
expect(exprObj.execute()).to.not.equal(exprObj.cachedValue)
}
})
it("throws an error if some variables are undefined.", function() {
expect(() => new Expression("x+n").execute()).to.throw("Undefined variable n.")
expect(() => new Expression("sin A.t").execute()).to.throw("Undefined variable A.")
expect(() => new Expression("f(3)").execute()).to.throw("Undefined variable f.")
})
})
describe("#simplify", function() {
it("returns an expression with just the result when no constant or object are used", function() {
expect(new Expression("2+2").simplify(Math.PI/2)).to.deep.equal(new Expression("4"))
expect(new Expression("x+3").simplify(5)).to.deep.equal(new Expression("8"))
expect(new Expression("sin x").simplify(Math.PI/2)).to.deep.equal(new Expression("1"))
expect(new Expression("0*e^x").simplify(Math.PI/2)).to.deep.equal(new Expression("0"))
})
it("returns a simplified version of the expression if constants are used", function() {
const original = new Expression("e^x").simplify(2)
const to = new Expression("e^2")
expect(original.toEditableString()).to.deep.equal(to.toEditableString())
})
})
describe("#toString", function() {
it("returns a human readable string of the expression", function() {
expect(new Expression("-2-3").toString()).to.equal("-5")
expect(new Expression("0.2+0.1").toString()).to.equal("0.3")
expect(new Expression("sin x").toString()).to.equal("sin x")
expect(new Expression("sin π").toString()).to.equal("sin π")
})
it("should add a sign if the option is passed", function() {
expect(new Expression("-2-3").toString(true)).to.equal("-5")
expect(new Expression("2+3").toString(true)).to.equal("+5")
})
})
describe("#executeExpression", function() {
it("directly computes the result of the expression with no variable", function() {
expect(executeExpression("2+3")).to.equal(5)
expect(executeExpression("sin (π/2)")).to.equal(1)
expect(executeExpression("e^3")).to.be.approximately(Math.pow(Math.E, 3), Number.EPSILON)
})
it("throws an error if variables are employed", function() {
expect(() => executeExpression("x+n")).to.throw("Undefined variable n.")
})
})
})

View file

@ -40,6 +40,17 @@ function QT_TRANSLATE_NOOP(category, string) {
return string return string
} }
/**
* Polyfilling Qt arg function.
* @param {string} argument
*/
String.prototype.arg = function(argument) {
for(let i = 0; i < 10; i++)
if(this.includes("%"+i))
return this.replaceAll("%" + i, argument)
throw new Error("Too many arguments used.")
}
function setup() { function setup() {
globalThis.Qt = { globalThis.Qt = {
rect rect
@ -47,8 +58,6 @@ function setup() {
globalThis.QT_TRANSLATE_NOOP = QT_TRANSLATE_NOOP globalThis.QT_TRANSLATE_NOOP = QT_TRANSLATE_NOOP
globalThis.qsTranslate = QT_TRANSLATE_NOOP globalThis.qsTranslate = QT_TRANSLATE_NOOP
String.prototype.arg = function() { return this; } // No need to reimplement it for now.
} }
setup() setup()

View file

@ -48,7 +48,7 @@ describe("Module/Latex", function() {
}) })
describe("#requestAsyncRender", function() { describe("#requestAsyncRender", function() {
it("should return a render result with a valid source, a width, and a height", async function() { it("returns a render result with a valid source, a width, and a height", async function() {
const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033") const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(data).to.be.an("object") expect(data).to.be.an("object")
expect(data.source).to.be.a("string") expect(data.source).to.be.a("string")
@ -57,7 +57,7 @@ describe("Module/Latex", function() {
expect(data.width).to.be.a("number") expect(data.width).to.be.a("number")
}) })
it("should call functions from the LaTeX module", async function() { it("calls functions from the LaTeX module", async function() {
const renderSyncSpy = spy.on(Latex, "renderSync") const renderSyncSpy = spy.on(Latex, "renderSync")
const renderAsyncSpy = spy.on(Latex, "renderAsync") const renderAsyncSpy = spy.on(Latex, "renderAsync")
Latex.supportsAsyncRender = true Latex.supportsAsyncRender = true
@ -84,7 +84,7 @@ describe("Module/Latex", function() {
}) })
describe("#findPrerendered", function() { describe("#findPrerendered", function() {
it("should return the same data as async render for the same markup, font size, and color", async function() { it("returns the same data as async render for the same markup, font size, and color", async function() {
const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033") const data = await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
const found = LatexAPI.findPrerendered("\\frac{x}{3}", 13, "#AA0033") const found = LatexAPI.findPrerendered("\\frac{x}{3}", 13, "#AA0033")
expect(found).to.not.be.null expect(found).to.not.be.null
@ -92,7 +92,7 @@ describe("Module/Latex", function() {
expect(found.width).to.equal(data.width) expect(found.width).to.equal(data.width)
}) })
it("should return null if the markup hasn't been prerendered with the same markup, font size, and color", async function() { it("returns null if the markup hasn't been prerendered with the same markup, font size, and color", async function() {
await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033") await LatexAPI.requestAsyncRender("\\frac{x}{3}", 13, "#AA0033")
expect(LatexAPI.findPrerendered("\\frac{y}{3}", 13, "#AA0033")).to.be.null expect(LatexAPI.findPrerendered("\\frac{y}{3}", 13, "#AA0033")).to.be.null
expect(LatexAPI.findPrerendered("\\frac{x}{3}", 12, "#AA0033")).to.be.null expect(LatexAPI.findPrerendered("\\frac{x}{3}", 12, "#AA0033")).to.be.null
@ -101,7 +101,7 @@ describe("Module/Latex", function() {
}) })
describe("#par", function() { describe("#par", function() {
it("should add parentheses to strings", function() { it("adds parentheses to strings", function() {
expect(LatexAPI.par("string")).to.equal("(string)") expect(LatexAPI.par("string")).to.equal("(string)")
expect(LatexAPI.par("aaaa")).to.equal("(aaaa)") expect(LatexAPI.par("aaaa")).to.equal("(aaaa)")
expect(LatexAPI.par("")).to.equal("()") expect(LatexAPI.par("")).to.equal("()")
@ -110,14 +110,14 @@ describe("Module/Latex", function() {
}) })
describe("#parif", function() { describe("#parif", function() {
it("should add parentheses to strings that contain one of the ones in the list", function() { it("adds parentheses to strings that contain one of the ones in the list", function() {
expect(LatexAPI.parif("string", ["+"])).to.equal("string") expect(LatexAPI.parif("string", ["+"])).to.equal("string")
expect(LatexAPI.parif("string+assert", ["+"])).to.equal("(string+assert)") expect(LatexAPI.parif("string+assert", ["+"])).to.equal("(string+assert)")
expect(LatexAPI.parif("string+assert", ["+", "-"])).to.equal("(string+assert)") expect(LatexAPI.parif("string+assert", ["+", "-"])).to.equal("(string+assert)")
expect(LatexAPI.parif("string-assert", ["+", "-"])).to.equal("(string-assert)") expect(LatexAPI.parif("string-assert", ["+", "-"])).to.equal("(string-assert)")
}) })
it("shouldn't add new parentheses to strings that contains one of the ones in the list if they already have one", function() { it("doesn't add new parentheses to strings that contains one of the ones in the list if they already have one", function() {
expect(LatexAPI.parif("(string+assert", ["+"])).to.equal("((string+assert)") expect(LatexAPI.parif("(string+assert", ["+"])).to.equal("((string+assert)")
expect(LatexAPI.parif("string+assert)", ["+"])).to.equal("(string+assert))") expect(LatexAPI.parif("string+assert)", ["+"])).to.equal("(string+assert))")
expect(LatexAPI.parif("(string+assert)", ["+"])).to.equal("(string+assert)") expect(LatexAPI.parif("(string+assert)", ["+"])).to.equal("(string+assert)")
@ -125,14 +125,14 @@ describe("Module/Latex", function() {
expect(LatexAPI.parif("(string-assert)", ["+", "-"])).to.equal("(string-assert)") expect(LatexAPI.parif("(string-assert)", ["+", "-"])).to.equal("(string-assert)")
}) })
it("shouldn't add parentheses to strings that does not contains one of the ones in the list", function() { it("doesn't add parentheses to strings that does not contains one of the ones in the list", function() {
expect(LatexAPI.parif("string", ["+"])).to.equal("string") expect(LatexAPI.parif("string", ["+"])).to.equal("string")
expect(LatexAPI.parif("string+assert", ["-"])).to.equal("string+assert") expect(LatexAPI.parif("string+assert", ["-"])).to.equal("string+assert")
expect(LatexAPI.parif("(string*assert", ["+", "-"])).to.equal("(string*assert") expect(LatexAPI.parif("(string*assert", ["+", "-"])).to.equal("(string*assert")
expect(LatexAPI.parif("string/assert)", ["+", "-"])).to.equal("string/assert)") expect(LatexAPI.parif("string/assert)", ["+", "-"])).to.equal("string/assert)")
}) })
it("should remove parentheses from strings that does not contains one of the ones in the list", function() { it("removes parentheses from strings that does not contains one of the ones in the list", function() {
expect(LatexAPI.parif("(string)", ["+"])).to.equal("string") expect(LatexAPI.parif("(string)", ["+"])).to.equal("string")
expect(LatexAPI.parif("(string+assert)", ["-"])).to.equal("string+assert") expect(LatexAPI.parif("(string+assert)", ["-"])).to.equal("string+assert")
expect(LatexAPI.parif("((string*assert)", ["+", "-"])).to.equal("(string*assert") expect(LatexAPI.parif("((string*assert)", ["+", "-"])).to.equal("(string*assert")
@ -164,47 +164,47 @@ describe("Module/Latex", function() {
"{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}", "{}_{4}", "{}_{5}", "{}_{6}", "{}_{7}", "{}_{8}", "{}_{9}", "{}_{0}",
"\\pi", "\\infty"] "\\pi", "\\infty"]
it("should convert unicode characters to their latex equivalent", function() { it("converts unicode characters to their latex equivalent", function() {
for(let i = 0; i < from.length; i++) for(let i = 0; i < from.length; i++)
expect(LatexAPI.variable(from[i])).to.include(to[i]) expect(LatexAPI.variable(from[i])).to.include(to[i])
}) })
it("should wrap within dollar signs when the option is included", function() { it("wraps within dollar signs when the option is included", function() {
for(let i = 0; i < from.length; i++) { for(let i = 0; i < from.length; i++) {
expect(LatexAPI.variable(from[i], false)).to.equal(to[i]) expect(LatexAPI.variable(from[i], false)).to.equal(to[i])
expect(LatexAPI.variable(from[i], true)).to.equal(`$${to[i]}$`) expect(LatexAPI.variable(from[i], true)).to.equal(`$${to[i]}$`)
} }
}) })
it("should be able to convert multiple of them", function() { it("can convert multiple of them", function() {
expect(LatexAPI.variable("α₂", false)).to.equal("\\alpha{}_{2}") expect(LatexAPI.variable("α₂", false)).to.equal("\\alpha{}_{2}")
expect(LatexAPI.variable("∞piΠ", false)).to.equal("\\infty\\pi\\Pi") expect(LatexAPI.variable("∞piΠ", false)).to.equal("\\infty\\pi\\Pi")
}) })
}) })
describe("#functionToLatex", function() { describe("#functionToLatex", function() {
it("should transform derivatives into latex fractions", function() { it("transforms derivatives into latex fractions", function() {
const d1 = LatexAPI.functionToLatex("derivative", ["'3t'", "'t'", "x+2"]) const d1 = LatexAPI.functionToLatex("derivative", ["'3t'", "'t'", "x+2"])
const d2 = LatexAPI.functionToLatex("derivative", ["f", "x+2"]) const d2 = LatexAPI.functionToLatex("derivative", ["f", "x+2"])
expect(d1).to.equal("\\frac{d3x}{dx}") expect(d1).to.equal("\\frac{d3x}{dx}")
expect(d2).to.equal("\\frac{df}{dx}(x+2)") expect(d2).to.equal("\\frac{df}{dx}(x+2)")
}) })
it("should transform integrals into latex limits", function() { it("transforms integrals into latex limits", function() {
const i1 = LatexAPI.functionToLatex("integral", ["0", "x", "'3y'", "'y'"]) const i1 = LatexAPI.functionToLatex("integral", ["0", "x", "'3y'", "'y'"])
const i2 = LatexAPI.functionToLatex("integral", ["1", "2", "f"]) const i2 = LatexAPI.functionToLatex("integral", ["1", "2", "f"])
expect(i1).to.equal("\\int\\limits_{0}^{x}3y dy") expect(i1).to.equal("\\int\\limits_{0}^{x}3y dy")
expect(i2).to.equal("\\int\\limits_{1}^{2}f(t) dt") expect(i2).to.equal("\\int\\limits_{1}^{2}f(t) dt")
}) })
it("should transform sqrt functions to sqrt latex", function() { it("transforms sqrt functions to sqrt latex", function() {
const sqrt1 = LatexAPI.functionToLatex("sqrt", ["(x+2)"]) const sqrt1 = LatexAPI.functionToLatex("sqrt", ["(x+2)"])
const sqrt2 = LatexAPI.functionToLatex("sqrt", ["\\frac{x}{2}"]) const sqrt2 = LatexAPI.functionToLatex("sqrt", ["\\frac{x}{2}"])
expect(sqrt1).to.equal("\\sqrt{x+2}") expect(sqrt1).to.equal("\\sqrt{x+2}")
expect(sqrt2).to.equal("\\sqrt{\\frac{x}{2}}") expect(sqrt2).to.equal("\\sqrt{\\frac{x}{2}}")
}) })
it("should transform abs, floor and ceil", function() { it("transforms abs, floor and ceil", function() {
const abs = LatexAPI.functionToLatex("abs", ["x+3"]) const abs = LatexAPI.functionToLatex("abs", ["x+3"])
const floor = LatexAPI.functionToLatex("floor", ["x+3"]) const floor = LatexAPI.functionToLatex("floor", ["x+3"])
const ceil = LatexAPI.functionToLatex("ceil", ["x+3"]) const ceil = LatexAPI.functionToLatex("ceil", ["x+3"])
@ -213,7 +213,7 @@ describe("Module/Latex", function() {
expect(ceil).to.equal("\\left\\lceil{x+3}\\right\\rceil") expect(ceil).to.equal("\\left\\lceil{x+3}\\right\\rceil")
}) })
it("should transform regular functions into latex", function() { it("transforms regular functions into latex", function() {
const f1 = LatexAPI.functionToLatex("f", ["x+3", true]) const f1 = LatexAPI.functionToLatex("f", ["x+3", true])
const f2 = LatexAPI.functionToLatex("h_1", ["10"]) const f2 = LatexAPI.functionToLatex("h_1", ["10"])
expect(f1).to.equal("\\mathrm{f}\\left(x+3, true\\right)") expect(f1).to.equal("\\mathrm{f}\\left(x+3, true\\right)")
@ -222,7 +222,7 @@ describe("Module/Latex", function() {
}) })
describe("#expression", function() { describe("#expression", function() {
it("should transform parsed expressions", function() { it("transforms parsed expressions", function() {
const expr = ExprEval.parse("(+1! == 2/2 ? sin [-2.2][0] : f(t)^(1+1-1) + sqrt(A.t)) * 3 % 1") const expr = ExprEval.parse("(+1! == 2/2 ? sin [-2.2][0] : f(t)^(1+1-1) + sqrt(A.t)) * 3 % 1")
const expected = "((((+1!))==(\\frac{2}{2}) ? (\\mathrm{sin}\\left(([(-2.2)][0])\\right)) : (\\mathrm{f}\\left(t\\right)^{1+1-1}+\\sqrt{A.t})) \\times 3) \\mathrm{mod} 1" const expected = "((((+1!))==(\\frac{2}{2}) ? (\\mathrm{sin}\\left(([(-2.2)][0])\\right)) : (\\mathrm{f}\\left(t\\right)^{1+1-1}+\\sqrt{A.t})) \\times 3) \\mathrm{mod} 1"
expect(LatexAPI.expression(expr.tokens)).to.equal(expected) expect(LatexAPI.expression(expr.tokens)).to.equal(expected)

View file

@ -26,5 +26,6 @@ import { expect } from "chai"
// import Objects from "../../src/module/objects.mjs" // import Objects from "../../src/module/objects.mjs"
// //
// describe("Module/Objects", function() { // describe("Module/Objects", function() {
// // describe("#getNewName", function() {
// })
// }) // })