Finished expr-eval testing
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Adsooi 2024-10-13 00:33:22 +02:00
parent edf4518494
commit 3a81441d0b
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
5 changed files with 260 additions and 132 deletions

View file

@ -111,7 +111,7 @@ function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
* In the given instructions, replaces variable by expr.
* @param {Instruction[]} tokens
* @param {string} variable
* @param {number} expr
* @param {ExprEvalExpression} expr
* @return {Instruction[]}
*/
function substitute(tokens, variable, expr) {
@ -487,18 +487,6 @@ export class ExprEvalExpression {
return evaluate(this.tokens, this, values)
}
/**
* Returns a list of symbols (string of characters) in the expressions.
* Can be functions, constants, or variables.
* @returns {string[]}
*/
symbols(options) {
options = options || {}
const vars = []
getSymbols(this.tokens, vars, options)
return vars
}
toString() {
return expressionToString(this.tokens, false)
}

View file

@ -120,18 +120,13 @@ export class Parser {
min: Polyfill.min,
max: Polyfill.max,
hypot: Math.hypot || Polyfill.hypot,
pyt: Math.hypot || Polyfill.hypot, // backward compat
pyt: Math.hypot || Polyfill.hypot,
pow: Math.pow,
atan2: Math.atan2,
"if": Polyfill.condition,
gamma: Polyfill.gamma,
"Γ": Polyfill.gamma,
roundTo: Polyfill.roundTo,
map: Polyfill.arrayMap,
fold: Polyfill.arrayFold,
filter: Polyfill.arrayFilter,
indexOf: Polyfill.stringOrArrayIndexOf,
join: Polyfill.arrayJoin
}
// These constants will automatically be replaced the MOMENT they are parsed.
@ -156,10 +151,6 @@ export class Parser {
return new ExprEvalExpression(instr, this)
}
evaluate(expr, variables) {
return this.parse(expr).evaluate(variables)
}
isOperatorEnabled(op) {
const optionName = optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op
const operators = this.options.operators || {}

View file

@ -210,9 +210,8 @@ export function gamma(n) {
}
export function stringOrArrayLength(s) {
if(Array.isArray(s)) {
if(Array.isArray(s))
return s.length
}
return String(s).length
}
@ -296,58 +295,6 @@ export function min(array) {
}
}
export function arrayMap(f, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to map is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to map is not an array."))
}
return a.map(function(x, i) {
return f(x, i)
})
}
export function arrayFold(f, init, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to fold is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to fold is not an array."))
}
return a.reduce(function(acc, x, i) {
return f(acc, x, i)
}, init)
}
export function arrayFilter(f, a) {
if(typeof f !== "function") {
throw new EvalError(qsTranslate("error", "First argument to filter is not a function."))
}
if(!Array.isArray(a)) {
throw new EvalError(qsTranslate("error", "Second argument to filter is not an array."))
}
return a.filter(function(x, i) {
return f(x, i)
})
}
export function stringOrArrayIndexOf(target, s) {
if(!(Array.isArray(s) || typeof s === "string")) {
throw new Error(qsTranslate("error", "Second argument to indexOf is not a string or array."))
}
return s.indexOf(target)
}
export function arrayJoin(sep, a) {
if(!Array.isArray(a)) {
throw new Error(qsTranslate("error", "Second argument to join is not an array."))
}
return a.join(sep)
}
export function sign(x) {
return ((x > 0) - (x < 0)) || +x
}

View file

@ -19,7 +19,7 @@
import { Module } from "./common.mjs"
import { Parser } from "../lib/expr-eval/parser.mjs"
const evalVariables = {
const EVAL_VARIABLES = {
// Variables not provided by expr-eval.js, needs to be provided manually
"pi": Math.PI,
"PI": Math.PI,
@ -42,7 +42,7 @@ class ExprParserAPI extends Module {
this.currentVars = {}
this.#parser = new Parser()
this.#parser.consts = Object.assign({}, this.#parser.consts, evalVariables)
this.#parser.consts = Object.assign({}, this.#parser.consts, EVAL_VARIABLES)
this.#parser.functions.integral = this.integral.bind(this)
this.#parser.functions.derivative = this.derivative.bind(this)

View file

@ -25,72 +25,274 @@ import { expect } from "chai"
import ExprEval from "../../src/module/expreval.mjs"
describe("Module/ExprEval", function() {
describe("#parse", function() {
const evaluate = (expr, vals) => ExprEval.parse(expr).evaluate(vals)
describe("#parse.evaluate", function() {
const evaluate = (expr, vals = {}) => ExprEval.parse(expr).evaluate(vals)
it("parses simple mathematical expressions", function() {
expect(evaluate("1", {})).to.equal(1)
expect(evaluate("-1", {})).to.equal(-1)
expect(evaluate("-(-1)", {})).to.equal(1)
expect(evaluate("+(-1)", {})).to.equal(-1)
expect(evaluate("3!", {})).to.equal(6)
expect(evaluate("1+1", {})).to.equal(2)
expect(evaluate("4*3", {})).to.equal(12)
expect(evaluate("64/4", {})).to.equal(16)
expect(evaluate("2^10", {})).to.equal(1024)
expect(evaluate("10%3", {})).to.equal(1)
expect(evaluate("10%3", {})).to.equal(1)
expect(evaluate(`"\\'\\"\\\\\\/\\b\\f\\n\\r\\t\\u3509"`)).to.equal(`'"\\/\b\f\n\r\t\u3509`)
expect(evaluate("1")).to.equal(1)
expect(evaluate(" 1 ")).to.equal(1)
expect(evaluate("0xFF")).to.equal(255)
expect(evaluate("0b11")).to.equal(3)
expect(evaluate("-1")).to.equal(-1)
expect(evaluate("-(-1)")).to.equal(1)
expect(evaluate("+(-1)")).to.equal(-1)
expect(evaluate("3!")).to.equal(6)
expect(evaluate("1+1")).to.equal(2)
expect(evaluate("4*3")).to.equal(12)
expect(evaluate("4•3")).to.equal(12)
expect(evaluate("64/4")).to.equal(16)
expect(evaluate("2^10")).to.equal(1024)
expect(evaluate("10%3")).to.equal(1)
expect(evaluate("10%3")).to.equal(1)
// Test priorities
expect(evaluate("10*10+10*10", {})).to.equal(200)
expect(evaluate("10/10+10/10", {})).to.equal(2)
expect(evaluate("10/10+10/10", {})).to.equal(2)
expect(evaluate("2^2-2^2", {})).to.equal(0)
expect(evaluate("(2^2-2)^2", {})).to.equal(4)
expect(evaluate("10*10+10*10")).to.equal(200)
expect(evaluate("10/10+10/10")).to.equal(2)
expect(evaluate("10/10+10/10")).to.equal(2)
expect(evaluate("2^2-2^2")).to.equal(0)
expect(evaluate("(2^2-2)^2")).to.equal(4)
})
it("parses equality and test statements", function() {
expect(evaluate("10%3 == 1 ? 2 : 1", {})).to.equal(2)
expect(evaluate("10%3 != 1 ? 2 : 1", {})).to.equal(1)
expect(evaluate("10 < 3 ? 2 : 1", {})).to.equal(1)
expect(evaluate("10 > 3 ? (2+1) : 1", {})).to.equal(3)
expect(evaluate("10 <= 3 ? 4 : 1", {})).to.equal(1)
expect(evaluate("10 >= 3 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10%3 == 1 ? 2 : 1")).to.equal(2)
expect(evaluate("not(10%3 == 1) ? 2 : 1")).to.equal(1)
expect(evaluate("10%3 != 1 ? 2 : 1")).to.equal(1)
expect(evaluate("10 < 3 ? 2 : 1")).to.equal(1)
expect(evaluate("10 > 3 ? (2+1) : 1")).to.equal(3)
expect(evaluate("10 <= 3 ? 4 : 1")).to.equal(1)
expect(evaluate("10 >= 3 ? 4 : 1")).to.equal(4)
// Check equality
expect(evaluate("10 < 10 ? 2 : 1", {})).to.equal(1)
expect(evaluate("10 > 10 ? 2 : 1", {})).to.equal(1)
expect(evaluate("10 <= 10 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10 >= 10 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10 < 10 ? 2 : 1")).to.equal(1)
expect(evaluate("10 > 10 ? 2 : 1")).to.equal(1)
expect(evaluate("10 <= 10 ? 4 : 1")).to.equal(4)
expect(evaluate("10 >= 10 ? 4 : 1")).to.equal(4)
// Check 'and' and 'or'
expect(evaluate("10 <= 3 and 10 < 10 ? 4 : 1", {})).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 10 ? 4 : 1", {})).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 20 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10 <= 3 or 10 < 10 ? 4 : 1", {})).to.equal(1)
expect(evaluate("10 <= 10 or 10 < 10 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10 <= 10 or 10 < 20 ? 4 : 1", {})).to.equal(4)
expect(evaluate("10 <= 3 and 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 and 10 < 20 ? 4 : 1")).to.equal(4)
expect(evaluate("10 <= 3 or 10 < 10 ? 4 : 1")).to.equal(1)
expect(evaluate("10 <= 10 or 10 < 10 ? 4 : 1")).to.equal(4)
expect(evaluate("10 <= 10 or 10 < 20 ? 4 : 1")).to.equal(4)
})
it("parses singular function operators (functions with one arguments and no parenthesis)", function() {
// Trigonometric functions
expect(evaluate("sin π", { })).to.be.approximately(0, Number.EPSILON)
expect(evaluate("cos π", { })).to.be.approximately(-1, Number.EPSILON)
expect(evaluate("tan π", { })).to.be.approximately(0, Number.EPSILON)
expect(evaluate("asin 1", { })).to.be.approximately(Math.PI/2, Number.EPSILON)
expect(evaluate("acos 1", { })).to.be.approximately(0, Number.EPSILON)
expect(evaluate("atan 1", { })).to.be.approximately(Math.PI/4, Number.EPSILON)
expect(evaluate("sinh 1", { })).to.be.approximately(Math.sinh(1), Number.EPSILON)
expect(evaluate("cosh 1", { })).to.be.approximately(Math.cosh(1), Number.EPSILON)
expect(evaluate("tanh 1", { })).to.be.approximately(Math.tanh(1), Number.EPSILON)
expect(evaluate("asinh 1", { })).to.be.approximately(Math.asinh(1), Number.EPSILON)
expect(evaluate("acosh 1", { })).to.be.approximately(Math.acosh(1), Number.EPSILON)
expect(evaluate("atanh 0.5", { })).to.be.approximately(Math.atanh(0.5), Number.EPSILON)
expect(evaluate("sin 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("cos 0")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("tan 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("asin 1")).to.be.approximately(Math.PI / 2, Number.EPSILON)
expect(evaluate("acos 1")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("atan 1")).to.be.approximately(Math.PI / 4, Number.EPSILON)
expect(evaluate("sinh 1")).to.be.approximately(Math.sinh(1), Number.EPSILON)
expect(evaluate("cosh 1")).to.be.approximately(Math.cosh(1), Number.EPSILON)
expect(evaluate("tanh 1")).to.be.approximately(Math.tanh(1), Number.EPSILON)
expect(evaluate("asinh 1")).to.be.approximately(Math.asinh(1), Number.EPSILON)
expect(evaluate("acosh 1")).to.be.approximately(Math.acosh(1), Number.EPSILON)
expect(evaluate("atanh 0.5")).to.be.approximately(Math.atanh(0.5), Number.EPSILON)
// Reverse trigonometric
expect(evaluate("asin sin 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acos cos 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atan tan 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("asinh sinh 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acosh cosh 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atanh tanh 1", { })).to.be.approximately(1, Number.EPSILON)
expect(evaluate("asin sin 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acos cos 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atan tan 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("asinh sinh 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("acosh cosh 1")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("atanh tanh 1")).to.be.approximately(1, Number.EPSILON)
// Other functions
expect(evaluate("sqrt 4")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("sqrt 2")).to.be.approximately(Math.sqrt(2), Number.EPSILON)
expect(evaluate("cbrt 27")).to.be.approximately(3, Number.EPSILON)
expect(evaluate("cbrt 14")).to.be.approximately(Math.cbrt(14), Number.EPSILON)
expect(evaluate("log 1")).to.be.approximately(Math.log(1), Number.EPSILON)
expect(evaluate("ln 1")).to.be.approximately(Math.log(1), Number.EPSILON)
expect(evaluate("log2 8")).to.be.approximately(3, Number.EPSILON)
expect(evaluate("log10 100")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("lg 100")).to.be.approximately(2, Number.EPSILON)
expect(evaluate("expm1 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("expm1 10")).to.be.approximately(Math.expm1(10), Number.EPSILON)
expect(evaluate("log1p 0")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("log1p 10")).to.be.approximately(Math.log1p(10), Number.EPSILON)
// Roundings/Sign transformations
expect(evaluate("abs -12.34")).to.equal(12.34)
expect(evaluate("abs 12.45")).to.equal(12.45)
expect(evaluate("ceil 12.45")).to.equal(13)
expect(evaluate("ceil 12.75")).to.equal(13)
expect(evaluate("ceil 12.0")).to.equal(12)
expect(evaluate("ceil -12.6")).to.equal(-12)
expect(evaluate("floor 12.45")).to.equal(12)
expect(evaluate("floor 12.75")).to.equal(12)
expect(evaluate("floor 12.0")).to.equal(12)
expect(evaluate("floor -12.2")).to.equal(-13)
expect(evaluate("round 12.45")).to.equal(12)
expect(evaluate("round 12.75")).to.equal(13)
expect(evaluate("round 12.0")).to.equal(12)
expect(evaluate("round -12.2")).to.equal(-12)
expect(evaluate("round -12.6")).to.equal(-13)
expect(evaluate("trunc 12.45")).to.equal(12)
expect(evaluate("trunc 12.75")).to.equal(12)
expect(evaluate("trunc 12.0")).to.equal(12)
expect(evaluate("trunc -12.2")).to.equal(-12)
expect(evaluate("exp 1")).to.be.approximately(Math.E, Number.EPSILON)
expect(evaluate("exp 10")).to.be.approximately(Math.pow(Math.E, 10), 1e-8)
expect(evaluate("length \"string\"")).to.equal(6)
expect(evaluate("sign 0")).to.equal(0)
expect(evaluate("sign -0")).to.equal(0)
expect(evaluate("sign -10")).to.equal(-1)
expect(evaluate("sign 80")).to.equal(1)
})
it("parses regular functions", function() {
for(let i = 0; i < 1000; i++) {
expect(evaluate("random()")).to.be.within(0, 1)
expect(evaluate("random(100)")).to.be.within(0, 100)
}
expect(evaluate("fac(3)")).to.equal(6)
expect(evaluate("fac(10)")).to.equal(3628800)
expect(evaluate("min(10, 20)")).to.equal(10)
expect(evaluate("min(-10, -20)")).to.equal(-20)
expect(evaluate("max(10, 20)")).to.equal(20)
expect(evaluate("max(-10, -20)")).to.equal(-10)
expect(evaluate("hypot(3, 4)")).to.equal(5)
expect(evaluate("pyt(30, 40)")).to.equal(50)
expect(evaluate("atan2(1, 1)")).to.be.approximately(Math.PI / 4, Number.EPSILON)
expect(evaluate("atan2(1, 0)")).to.be.approximately(Math.PI / 2, Number.EPSILON)
expect(evaluate("atan2(0, 1)")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("if(10 == 10, 1, 0)")).to.be.approximately(1, Number.EPSILON)
expect(evaluate("if(10 != 10, 1, 0)")).to.be.approximately(0, Number.EPSILON)
expect(evaluate("gamma(10) == 9!")).to.be.true
expect(evaluate("Γ(30) == 29!")).to.be.true
expect(evaluate("Γ(25) == 23!")).to.be.false
expect(evaluate("roundTo(26.04)")).to.equal(26)
expect(evaluate("roundTo(26.04, 2)")).to.equal(26.04)
expect(evaluate("roundTo(26.04836432123, 5)")).to.equal(26.04836)
expect(evaluate("roundTo(26.04836432123, 5)")).to.equal(26.04836)
})
it("parses arrays and access their members", function() {
expect(evaluate("[6, 7, 9]")).to.have.lengthOf(3)
expect(evaluate("[6, 7, 9]")).to.deep.equal([6, 7, 9])
expect(evaluate("[6, \"8\", 9]")).to.have.lengthOf(3)
expect(evaluate("[6, 7%2]")).to.deep.equal([6, 1])
// Access array indices
expect(evaluate("[6, 7][1]")).to.equal(7)
expect(evaluate("[6, 7, 8, 9, 10][2*2-1]")).to.equal(9)
})
it("can apply functions to arrays", function() {
expect(evaluate("length [6, 7, 9]")).to.equal(3)
expect(evaluate("length [6, 7, 8, 9]")).to.equal(4)
expect(evaluate("[6, 7, 9]||[10,11,12]")).to.deep.equal([6, 7, 9, 10, 11, 12])
expect(evaluate("6 in [6, 7, 9]")).to.be.true
expect(evaluate("2 in [6, 7, 9]")).to.be.false
expect(evaluate("min([10, 6, 7, 8, 9])")).to.equal(6)
expect(evaluate("max([6, 7, 8, 9, 2])")).to.equal(9)
})
it("throws errors when invalid function parameters are provided", function() {
expect(() => evaluate("max()")).to.throw()
expect(() => evaluate("min()")).to.throw()
})
it("parses constants", function() {
expect(evaluate("pi")).to.equal(Math.PI)
expect(evaluate("PI")).to.equal(Math.PI)
expect(evaluate("π")).to.equal(Math.PI)
expect(evaluate("e")).to.equal(Math.E)
expect(evaluate("E")).to.equal(Math.E)
expect(evaluate("true")).to.be.true
expect(evaluate("false")).to.be.false
// expect(evaluate("∞")).to.equal(Math.Infinity)
// expect(evaluate("infinity")).to.equal(Math.Infinity)
// expect(evaluate("Infinity")).to.equal(Math.Infinity)
})
it("can be provided variables", function() {
const u = [1, 2, 3, 4]
const x = 10
const s_ = "string"
const f = (x) => x * 2
expect(evaluate("u", { u })).to.deep.equal([...u])
expect(evaluate("x", { x })).to.equal(x)
expect(evaluate("s_", { s_ })).to.equal(s_)
expect(evaluate("f", { f })).to.equal(f)
expect(evaluate("b", { b: true })).to.equal(true)
expect(evaluate("u[1]", { u })).to.equal(u[1])
expect(evaluate("x/2", { x })).to.equal(x / 2)
expect(evaluate("f(2)", { f })).to.equal(f(2))
expect(evaluate("if(x == f(2), u[0], s_)", { x, u, s_, f })).to.equal(s_)
})
it("can be provided objects", function() {
const obj = { execute: (x) => x * 3, x: 10, y: { cached: true, execute: () => 20 } }
expect(evaluate("O(3)+O(2)", { O: obj })).to.equal(9 + 6)
expect(evaluate("O.x+O.y", { O: obj })).to.equal(30)
})
it("throws errors when trying to use variables wrongly", function() {
const obj = { execute: (x) => x * 3 }
expect(() => evaluate("O()", { O: obj })).to.throw()
expect(() => evaluate("O.x", { O: obj })).to.throw()
expect(() => evaluate("x()", { x: 10 })).to.throw()
expect(() => evaluate("x")).to.throw()
expect(() => evaluate("n")).to.throw()
})
it("can do it all at once", function() {
const obj = { execute: (x) => x * 3, x: 20 }
const u = [1, 2, 3, 4]
const x = 10
const s = "string"
const expr = "random(e) <= e ? fac(x)+u[2]+O(pi) : O.x+length s"
expect(evaluate(expr, { x, u, s, O: obj })).to.equal(3628803 + obj.execute(Math.PI))
})
it("cannot parse invalid expressions", function() {
expect(() => evaluate("1+")).to.throw()
expect(() => evaluate("@")).to.throw()
expect(() => evaluate("]")).to.throw()
expect(() => evaluate("")).to.throw()
expect(() => evaluate(`"\\u35P2"`)).to.throw()
expect(() => evaluate(`"\\x"`)).to.throw()
})
})
describe("#parse.toString", function() {
it("can be converted back into a string without changes", function() {
const expressions = ["pi+2*(e+2)^4", "sin(1+2!+pi+cos -3)^2", "[2,3,4][(2-1)*2]", "true ? false : true"]
for(const ogString of expressions) {
const expr = ExprEval.parse(ogString)
const convertedString = expr.toString()
expect(ExprEval.parse(convertedString)).to.deep.equal(expr) // Can be reparsed just the same
}
})
})
describe("#parse.substitute", function() {
const parsed = ExprEval.parse("if(x == 0, 1, 2+x)")
it("can substitute a variable for a number", function() {
expect(parsed.substitute("x", 10).evaluate({})).to.equal(12)
expect(parsed.substitute("x", 0).evaluate({})).to.equal(1)
})
it("can substitute a variable for another", function() {
expect(parsed.substitute("x", "b").evaluate({ b: 10 })).to.equal(12)
expect(parsed.substitute("x", "b").evaluate({ b: 0 })).to.equal(1)
})
it("can substitute a variable for an expression", function() {
expect(parsed.substitute("x", "sin α").evaluate({ "α": Math.PI / 2 })).to.be.approximately(3, Number.EPSILON)
expect(parsed.substitute("x", "sin α").evaluate({ "α": 0 })).to.equal(1)
expect(parsed.substitute("x", "α == 1 ? 0 : 1").evaluate({ "α": 1 })).to.equal(1)
})
})
describe("#parse.variables", function() {
it("can list all parsed undefined variables", function() {
expect(ExprEval.parse("a+b+x+pi+sin(b)").variables()).to.deep.equal(["a", "b", "x"])
})
})
describe("#parse.toJSFunction", function() {
const func = ExprEval.parse("not(false) ? a+b+x+1/x : x!+random()+A.x+[][0]").toJSFunction("x", { a: "10", b: "0" })
expect(func(10)).to.equal(20.1)
expect(func(20)).to.equal(30.05)
})
@ -143,7 +345,7 @@ describe("Module/ExprEval", function() {
describe("#derivative", function() {
const DELTA = 1e-5
it("returns the derivative value between two integers", function() {
it("returns the derivative value of a function at a given number", function() {
expect(ExprEval.derivative("1", "t", 2)).to.be.approximately(0, DELTA)
expect(ExprEval.derivative("t", "t", 2)).to.be.approximately(1, DELTA)
expect(ExprEval.derivative("t^2", "t", 2)).to.be.approximately(4, DELTA)