LogarithmPlotter/qml/js/mathlib.js
2021-03-08 23:12:10 +01:00

646 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Logarithm Graph Creator - Create graphs with logarithm scales.
* Copyright (C) 2020 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.pragma library
.import "expr-eval.js" as ExprEval
.import "utils.js" as Utils
var evalVariables = { // Variables not provided by expr-eval.js, needs to be provided manualy
"pi": Math.PI,
"π": Math.PI,
"inf": Infinity,
"Infinity": Infinity,
"∞": Infinity,
"e": Math.E,
"E": Math.E
}
var currentVars = {}
const parser = new ExprEval.Parser()
parser.functions.integral = function(a, b, f, variable) {
// https://en.wikipedia.org/wiki/Simpson%27s_rule
f = parser.parse(f).toJSFunction(variable, currentVars)
return (b-a)/6*(f(a)+4*f((a+b)/2)+f(b))
}
class Expression {
constructor(expr) {
this.expr = expr
this.calc = parser.parse(expr).simplify()
this.cached = this.isConstant()
this.cachedValue = this.cached ? this.calc.evaluate(evalVariables) : null
}
isConstant() {
return !this.expr.includes("x") && !this.expr.includes("n")
}
execute(x = 1) {
if(this.cached) return this.cachedValue
currentVars = Object.assign({'x': x}, evalVariables)
return this.calc.evaluate(currentVars)
}
simplify(x) {
var expr = this.calc.substitute('x', x).simplify()
if(expr.evaluate(evalVariables) == 0) return '0'
var str = Utils.makeExpressionReadable(expr.toString());
if(str != undefined && str.match(/^\d*\.\d+$/)) {
if(str.split('.')[1].split('0').length > 7) {
// Likely rounding error
str = parseFloat(str.substring(0, str.length-1)).toString();
}
}
return str
}
duplicate() {
return new Expression(this.toEditableString())
}
toEditableString() {
return this.calc.toString()
}
toString(forceSign=false) {
var str = Utils.makeExpressionReadable(this.calc.toString())
if(str[0] != '-' && forceSign) str = '+' + str
return str
}
}
function executeExpression(expr){
return (new Expression(expr.toString())).execute()
}
class Sequence extends Expression {
constructor(name, baseValues = {}, valuePlus = 1, expr = "") {
// u[n+valuePlus] = expr
super(expr)
this.name = name
this.baseValues = baseValues
this.calcValues = Object.assign({}, baseValues)
for(var n in this.calcValues)
if(['string', 'number'].includes(typeof this.calcValues[n]))
this.calcValues[n] = parser.parse(this.calcValues[n].toString()).simplify().evaluate(evalVariables)
this.valuePlus = parseInt(valuePlus)
}
isConstant() {
return this.expr.indexOf("n") == -1
}
execute(n = 1) {
if(n in this.calcValues)
return this.calcValues[n]
this.cache(n)
return this.calcValues[n]
}
simplify(n = 1) {
if(n in this.calcValues)
return Utils.makeExpressionReadable(this.calcValues[n].toString())
this.cache(n)
return Utils.makeExpressionReadable(this.calcValues[n].toString())
}
cache(n = 1) {
var str = Utils.simplifyExpression(this.calc.substitute('n', n-this.valuePlus).toString())
var expr = parser.parse(str).simplify()
var l = {'n': n-this.valuePlus} // Just in case, add n (for custom functions)
l[this.name] = this.calcValues
currentVars = Object.assign(l, evalVariables)
this.calcValues[n] = expr.evaluate(currentVars)
}
toString(forceSign=false) {
var str = Utils.makeExpressionReadable(this.calc.toString())
if(str[0] != '-' && forceSign) str = '+' + str
var subtxt = this.valuePlus == 0 ? 'ₙ' : Utils.textsub('n+' + this.valuePlus)
var ret = `${this.name}${subtxt} = ${str}${this.baseValues.length == 0 ? '' : "\n"}`
ret += Object.keys(this.baseValues).map(
n => `${this.name}${Utils.textsub(n)} = ${this.baseValues[n]}`
).join('; ')
return ret
}
}
// Domains
class Domain {
constructor() {}
includes(x) { return false }
toString() { return '???' }
union(domain) { return domain }
intersection(domain) { return this }
static import(frm) {
switch(frm.trim().toUpperCase()) {
case "R":
case "":
return Domain.R
break;
case "RE":
case "R*":
case "*":
return Domain.RE
break;
case "RP":
case "R+":
case "ℝ⁺":
return Domain.RP
break;
case "RM":
case "R-":
case "ℝ⁻":
return Domain.RM
break;
case "RPE":
case "REP":
case "R+*":
case "R*+":
case "*⁺":
case "ℝ⁺*":
return Domain.RPE
break;
case "RME":
case "REM":
case "R-*":
case "R*-":
case "ℝ⁻*":
case "*⁻":
return Domain.RME
break;
case "":
case "N":
case "ZP":
case "ℤ⁺":
return Domain.N
break;
case "NLOG":
case "ℕˡᵒᵍ":
return Domain.NLog
break;
case "NE":
case "NP":
case "N*":
case "N+":
case "*":
case "ℕ⁺":
case "ZPE":
case "ZEP":
case "Z+*":
case "Z*+":
case "ℤ⁺*":
case "*⁺":
return Domain.NE
break;
case "Z":
case "":
return Domain.Z
break;
case "ZM":
case "Z-":
case "ℤ⁻":
return Domain.ZM
break;
case "ZME":
case "ZEM":
case "Z-*":
case "Z*-":
case "ℤ⁻*":
case "*⁻":
return Domain.ZME
break;
case "ZE":
case "Z*":
case "*":
return Domain.ZE
break;
default:
return new EmptySet()
break;
}
}
}
class EmptySet extends Domain {
includes(x) { return false }
toString() { return "∅" }
union(domain) { return domain }
intersection(domain) { return this }
static import(frm) { return new EmptySet() }
}
class Range extends Domain {
constructor(begin, end, openBegin, openEnd) {
super()
if(typeof begin == 'number' || typeof begin == 'string') begin = new Expression(begin.toString())
this.begin = begin
if(typeof end == 'number' || typeof end == 'string') end = new Expression(end.toString())
this.end = end
this.openBegin = openBegin
this.openEnd = openEnd
this.displayName = (openBegin ? "]" : "[") + begin.toString() + ";" + end.toString() + (openEnd ? "[" : "]")
}
includes(x) {
if(typeof x == 'string') x = executeExpression(x)
return ((this.openBegin && x > this.begin.execute()) || (!this.openBegin && x >= this.begin.execute())) &&
((this.openEnd && x < this.end.execute()) || (!this.openEnd && x <= this.end.execute()))
}
toString() {
return this.displayName
}
union(domain) {
if(domain instanceof EmptySet) return this
if(domain instanceof DomainSet) return domain.union(this)
if(domain instanceof UnionDomain) return domain.union(this)
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
if(domain instanceof MinusDomain) return new UnionDomain(this, domain)
if(domain instanceof Range) return new UnionDomain(this, domain)
}
intersection(domain) {
if(domain instanceof EmptySet) return domain
if(domain instanceof DomainSet) return domain.intersection(this)
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof IntersectionDomain) return domain.intersection(this)
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
if(domain instanceof Range) return new IntersectionDomain(this, domain)
}
static import(frm) {
var openBegin = frm.trim().charAt(0) == "]"
var openEnd = frm.trim().charAt(frm.length -1) == "["
var [begin, end] = frm.substr(1, frm.length-2).split(";")
return new Range(begin.trim(), end.trim(), openBegin, openEnd)
}
}
class SpecialDomain extends Domain {
// For special domains (N, Z...)
// isValidExpr is function returning true when number is in domain
// false when it isn't.
// nextValue provides the next positive value in the domain
// after the one given.
constructor(displayName, isValid, next = x => true, previous = x => true,
moveSupported = true) {
super()
this.displayName = displayName
this.isValid = isValid
this.nextValue = next
this.prevValue = previous
this.moveSupported = moveSupported
}
includes(x) {
if(typeof x == 'string') x = executeExpression(x)
return this.isValid(x)
}
next(x) {
if(typeof x == 'string') x = executeExpression(x)
return this.nextValue(x)
}
previous(x) {
if(typeof x == 'string') x = executeExpression(x)
return this.prevValue(x)
}
toString() {
return this.displayName
}
union(domain) {
if(domain instanceof EmptySet) return this
if(domain instanceof DomainSet) return domain.union(this)
if(domain instanceof UnionDomain) return new UnionDomain(this, domain)
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
if(domain instanceof MinusDomain) return new UnionDomain(this, domain)
if(domain instanceof Range) return new UnionDomain(this, domain)
}
intersection(domain) {
if(domain instanceof EmptySet) return domain
if(domain instanceof DomainSet) return domain.intersection(this)
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof IntersectionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
if(domain instanceof Range) return new IntersectionDomain(this, domain)
}
}
class DomainSet extends SpecialDomain {
constructor(values) {
super('', x => true, x => x, true)
console.log(values)
var newVals = {}
this.executedValues = []
for(var value of values) {
var expr = new Expression(value.toString())
var ex = expr.execute()
newVals[ex] = expr
this.executedValues.push(ex)
}
this.executedValues.sort((a,b) => a-b)
this.values = this.executedValues.map(val => newVals[val])
}
includes(x) {
if(typeof x == 'string') x = executeExpression(x)
for(var value of this.values)
if(x == value.execute()) return true
return false
}
next(x) {
if(typeof x == 'string') x = executeExpression(x)
if(x < this.executedValues[0]) return this.executedValues[0]
for(var i = 1; i < this.values.length; i++) {
var prevValue = this.executedValues[i-1]
var value = this.executedValues[i]
if(x >= prevValue && x < value) return value
}
return null
}
previous(x) {
if(typeof x == 'string') x = executeExpression(x)
if(x > this.executedValues[this.executedValues.length-1])
return this.executedValues[this.executedValues.length-1]
for(var i = 1; i < this.values.length; i++) {
var prevValue = this.executedValues[i-1]
var value = this.executedValues[i]
if(x > prevValue && x <= value) return prevValue
}
return null
}
toString() {
return "{" + this.values.join(";") + "}"
}
union(domain) {
if(domain instanceof EmptySet) return this
if(domain instanceof DomainSet) {
var newValues = []
var values = this.values.concat(domain.values).filter(function(val){
newValues.push(val.execute())
return newValues.indexOf(val.execute()) == newValues.length - 1
})
return new DomainSet(values)
}
var notIncludedValues = []
for(var value in this.values) {
var value = this.executedValues[i]
if(domain instanceof Range) {
if(domain.begin.execute() == value && domain.openBegin) {
domain.openBegin = false
}
if(domain.end.execute() == value && domain.openEnd) {
domain.openEnd = false
}
}
if(!domain.includes(value))
notIncludedValues.push(this.values[i].toEditableString())
}
if(notIncludedValues.length == 0) return domain
return new UnionDomain(domain, new DomainSet(notIncludedValues))
}
intersection(domain) {
if(domain instanceof EmptySet) return domain
if(domain instanceof DomainSet) {
var domValues = domain.values.map(expr => expr.execute())
this.values = this.values.filter(function(val){
return domValues.indexOf(val.execute()) >= 0
})
return this
}
var includedValues = []
for(var i in this.values) {
var value = this.executedValues[i]
if(domain instanceof Range) {
if(domain.begin.execute() == value && !domain.openBegin) {
domain.openBegin = false
}
if(domain.end.execute() == value && !domain.openEnd) {
domain.openEnd = false
}
}
if(domain.includes(value))
includedValues.push(this.values[i].toEditableString())
}
if(includedValues.length == 0) return new EmptySet()
if(includedValues.length == this.values.length) return this
return new IntersectionDomain(domain, new DomainSet(includedValues))
}
static import(frm) {
return new DomainSet(frm.substr(1, frm.length-2).split(";"))
}
}
class UnionDomain extends Domain {
constructor(dom1, dom2) {
super()
this.dom1 = dom1
this.dom2 = dom2
}
includes(x) {
return this.dom1.includes(x) || this.dom2.includes(x)
}
toString() {
return this.dom1.toString() + " " + this.dom2.toString()
}
union(domain) {
if(domain instanceof EmptySet) return this
if(domain instanceof DomainSet) return domain.union(this)
if(domain instanceof Range) return domain.union(this)
if(domain instanceof UnionDomain) return new UnionDomain(this, domain)
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
if(domain instanceof MinusDomain) return new MinusDomain(this, domain)
}
intersection(domain) {
if(domain instanceof EmptySet) return domain
if(domain instanceof DomainSet) return domain.intersection(this)
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof IntersectionDomain) return this.dom1.intersection(domain.dom1).intersection(this.dom2).intersection(domain.dom2)
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
}
static import(frm) {
var domains = frm.trim().split("")
if(domains.length == 1) domains = frm.trim().split("U") // Fallback
var dom1 = parseDomain(domains.pop())
var dom2 = parseDomain(domains.join(''))
return dom1.union(dom2)
}
}
class IntersectionDomain extends Domain {
constructor(dom1, dom2) {
super()
this.dom1 = dom1
this.dom2 = dom2
}
includes(x) {
return this.dom1.includes(x) && this.dom2.includes(x)
}
toString() {
return this.dom1.toString() + " ∩ " + this.dom2.toString()
}
union(domain) {
if(domain instanceof EmptySet) return this
if(domain instanceof DomainSet) return domain.union(this)
if(domain instanceof Range) return domain.union(this)
if(domain instanceof UnionDomain) return this.dom1.union(domain.dom1).union(this.dom2).union(domain.dom2)
if(domain instanceof IntersectionDomain) return new UnionDomain(this, domain)
if(domain instanceof MinusDomain) return new MinusDomain(this, domain)
}
intersection(domain) {
if(domain instanceof EmptySet) return domain
if(domain instanceof DomainSet) return domain.intersection(this)
if(domain instanceof UnionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof IntersectionDomain) return new IntersectionDomain(this, domain)
if(domain instanceof MinusDomain) return new IntersectionDomain(this, domain)
}
static import(frm) {
var domains = frm.trim().split("∩")
var dom1 = parseDomain(domains.pop())
var dom2 = parseDomain(domains.join('∩'))
return dom1.intersection(dom2)
}
}
class MinusDomain extends Domain {
constructor(dom1, dom2) {
super()
this.dom1 = dom1
this.dom2 = dom2
}
includes(x) {
return this.dom1.includes(x) && !this.dom2.includes(x)
}
toString() {
return this.dom1.toString() + "" + this.dom2.toString()
}
static import(frm) {
var domains = frm.trim().split("")
if(domains.length == 1) domains = frm.trim().split("\\") // Fallback
var dom1 = parseDomain(domains.pop())
var dom2 = parseDomain(domains.join(''))
return new MinusDomain(dom1, dom2)
}
}
Domain.RE = new MinusDomain("R", "{0}")
Domain.RE.displayName = "*"
Domain.R = new Range(-Infinity,Infinity,true,true)
Domain.R.displayName = ""
Domain.RP = new Range(0,Infinity,true,false)
Domain.RP.displayName = "ℝ⁺"
Domain.RM = new Range(-Infinity,0,true,false)
Domain.RM.displayName = "ℝ⁻"
Domain.RPE = new Range(0,Infinity,true,true)
Domain.RPE.displayName = "ℝ⁺*"
Domain.RME = new Range(-Infinity,0,true,true)
Domain.RME.displayName = "ℝ⁻*"
Domain.N = new SpecialDomain('', x => x%1==0 && x >= 0,
x => Math.max(Math.floor(x)+1, 0),
x => Math.max(Math.ceil(x)-1, 0))
Domain.NE = new SpecialDomain('*', x => x%1==0 && x > 0,
x => Math.max(Math.floor(x)+1, 1),
x => Math.max(Math.ceil(x)-1, 1))
Domain.Z = new SpecialDomain('', x => x%1==0, x => Math.floor(x)+1, x => Math.ceil(x)-1)
Domain.ZE = new SpecialDomain('*', x => x%1==0 && x != 0,
x => Math.floor(x)+1 == 0 ? Math.floor(x)+2 : Math.floor(x)+1,
x => Math.ceil(x)-1 == 0 ? Math.ceil(x)-2 : Math.ceil(x)-1)
Domain.ZM = new SpecialDomain('ℤ⁻', x => x%1==0 && x <= 0,
x => Math.min(Math.floor(x)+1, 0),
x => Math.min(Math.ceil(x)-1, 0))
Domain.ZME = new SpecialDomain('ℤ⁻*', x => x%1==0 && x < 0,
x => Math.min(Math.floor(x)+1, -1),
x => Math.min(Math.ceil(x)-1, -1))
Domain.NLog = new SpecialDomain('ℕˡᵒᵍ',
x => x/Math.pow(10, x.toString().length-1) % 1 == 0 && x > 0,
function(x) {
var x10pow = Math.pow(10, x.toString().length-1)
return Math.max(1, (Math.floor(x/x10pow)+1)*x10pow)
},
function(x) {
var x10pow = Math.pow(10, x.toString().length-1)
return Math.max(1, (Math.ceil(x/x10pow)-1)*x10pow)
})
var refedDomains = []
function parseDomain(domain) {
if(!domain.includes(')') && !domain.includes('(')) return parseDomainSimple(domain)
var domStr
while((domStr = /\(([^)(]+)\)/.exec(domain)) !== null) {
var dom = parseDomainSimple(domStr[1].trim());
domain = domain.replace(domStr[0], 'D' + refedDomains.length)
refedDomains.push(dom)
}
return parseDomainSimple(domain)
}
function parseDomainSimple(domain) {
domain = domain.trim()
if(domain.includes("U") || domain.includes("")) return UnionDomain.import(domain)
if(domain.includes("∩")) return IntersectionDomain.import(domain)
if(domain.includes("") || domain.includes("\\")) return MinusDomain.import(domain)
if(domain.charAt(0) == "{" && domain.charAt(domain.length -1) == "}") return DomainSet.import(domain)
if(domain.includes("]") || domain.includes("[")) return Range.import(domain)
if(["R", "", "N", "", "Z", ""].some(str => domain.toUpperCase().includes(str)))
return Domain.import(domain)
if(domain[0] == 'D') return refedDomains[parseInt(domain.substr(1))]
return new EmptySet()
}