@mathigon/hilbert
Advanced tools
Comparing version 0.1.1 to 0.2.1
@@ -50,6 +50,10 @@ 'use strict'; | ||
static startingOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot start or end with a “${x}”.`); | ||
static startOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot start with a “${x}”.`); | ||
} | ||
static endOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot end with a “${x}”.`); | ||
} | ||
static consecutiveOperators(x, y) { | ||
@@ -80,2 +84,22 @@ return new ExprError('SyntaxError', `A “${x}” cannot be followed by a “${y}”.`); | ||
/** | ||
* Function wrapper that modifies a function to cache its return values. This | ||
* is useful for performance intensive functions which are called repeatedly | ||
* with the same arguments. However it can reduce performance for functions | ||
* which are always called with different arguments. Note that argument | ||
* comparison doesn't not work with Objects or nested arrays. | ||
* @param {Function} fn | ||
* @returns {Function} | ||
*/ | ||
function cache(fn) { | ||
let cached = new Map(); | ||
return function(...args) { | ||
let argString = args.join('--'); | ||
if (!cached.has(argString)) cached.set(argString, fn(...args)); | ||
return cached.get(argString); | ||
}; | ||
} | ||
// ============================================================================= | ||
@@ -115,2 +139,12 @@ | ||
/** | ||
* Join multiple Arrays | ||
* @param {*[]...} arrays | ||
* @returns {*[]} | ||
*/ | ||
function join(...arrays) { | ||
return [].concat(...arrays); | ||
} | ||
// ============================================================================= | ||
@@ -135,3 +169,68 @@ | ||
// ============================================================================ | ||
// Fermat.js | Number Theory | ||
// (c) Mathigon | ||
// ============================================================================ | ||
// ----------------------------------------------------------------------------- | ||
// Simple Functions | ||
const tolerance = 0.000001; | ||
/** | ||
* Checks if two numbers are nearly equals. | ||
* @param {number} x | ||
* @param {number} y | ||
* @param {?number} t The allowed tolerance | ||
* @returns {boolean} | ||
*/ | ||
function nearlyEquals(x, y, t = tolerance) { | ||
return Math.abs(x - y) < t; | ||
} | ||
// ============================================================================ | ||
// ============================================================================= | ||
// ============================================================================ | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ----------------------------------------------------------------------------- | ||
// Angles | ||
const twoPi = 2 * Math.PI; | ||
// ============================================================================ | ||
// ============================================================================ | ||
// ============================================================================ | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================ | ||
// ============================================================================= | ||
// ============================================================================= | ||
// ============================================================================= | ||
// Hilbert.js | Symbols | ||
@@ -143,2 +242,8 @@ // (c) Mathigon | ||
const CONSTANTS = { | ||
pi: Math.PI, | ||
π: Math.PI, | ||
e: Math.E | ||
}; | ||
const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'}; | ||
@@ -151,2 +256,4 @@ | ||
'+-': '±', | ||
'–': '−', | ||
'-': '−', | ||
xx: '×', | ||
@@ -233,3 +340,3 @@ sum: '∑', | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~^_…'; | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…'; | ||
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS); | ||
@@ -241,3 +348,119 @@ const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; | ||
const PRECEDENCE = words('+ - * × · // ^'); | ||
/** | ||
* Maths Expression | ||
*/ | ||
class ExprElement { | ||
/** | ||
* Evaluates an expression using a given map of variables and functions. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {number|null} | ||
*/ | ||
evaluate(_vars={}) { return null; } | ||
/** | ||
* Substitutes a new expression for a variable. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {Expression} | ||
*/ | ||
substitute(_vars={}) { return this; } | ||
/** | ||
* Returns the simplest mathematically equivalent expression. | ||
* @returns {Expression} | ||
*/ | ||
get simplified() { return this; } | ||
/** | ||
* Returns a list of all variables used in the expression. | ||
* @returns {String[]} | ||
*/ | ||
get variables() { return []; } | ||
/** | ||
* Returns a list of all functions called by the expression. | ||
* @returns {String[]} | ||
*/ | ||
get functions() { return []; } | ||
/** | ||
* Collapses all terms into functions. | ||
* @returns {Expression} | ||
*/ | ||
collapse() { return this; } | ||
/** | ||
* Converts the expression to a plain text string. | ||
* @returns {string} | ||
*/ | ||
toString() { return ''; } | ||
/** | ||
* Converts the expression to a MathML string. | ||
* @param {Object.<String, Function>=} _custom | ||
* @returns {string} | ||
*/ | ||
toMathML(_custom={}) { return ''; } | ||
} | ||
// ----------------------------------------------------------------------------- | ||
class ExprNumber extends ExprElement { | ||
constructor(n) { super(); this.n = n; } | ||
evaluate() { return this.n; } | ||
toString() { return '' + this.n; } | ||
toMathML() { return `<mn>${this.n}</mn>`; } | ||
} | ||
class ExprIdentifier extends ExprElement { | ||
constructor(i) { super(); this.i = i; } | ||
evaluate(vars={}) { | ||
if (this.i in vars) return vars[this.i]; | ||
if (this.i in CONSTANTS) return CONSTANTS[this.i]; | ||
throw ExprError.undefinedVariable(this.i); | ||
} | ||
substitute(vars={}) { return vars[this.i] || this; } | ||
get variables() { return [this.i]; } | ||
toString() { return this.i; } | ||
toMathML() { return `<mi>${this.i}</mi>`; } | ||
} | ||
class ExprString extends ExprElement { | ||
constructor(s) { super(); this.s = s; } | ||
evaluate() { throw ExprError.undefinedVariable(this.s); } | ||
toString() { return '"' + this.s + '"'; } | ||
toMathML() { return `<mtext>${this.s}</mtext>`; } | ||
} | ||
class ExprSpace extends ExprElement { | ||
toString() { return ' '; } | ||
toMathML() { return `<mspace/>`; } | ||
} | ||
class ExprOperator extends ExprElement { | ||
constructor(o) { super(); this.o = o; } | ||
toString() { return this.o.replace('//', '/'); } | ||
toMathML() { return `<mo value="${this.toString()}">${this.toString()}</mo>`; } | ||
get functions() { return [this.o]; } | ||
} | ||
class ExprTerm extends ExprElement { | ||
constructor(items) { super(); this.items = items; } | ||
evaluate(vars={}) { return this.collapse().evaluate(vars); } | ||
substitute(vars={}) { return this.collapse().substitute(vars); } | ||
get simplified() { return this.collapse().simplified; } | ||
get variables() { return unique(join(...this.items.map(i => i.variables))); } | ||
get functions() { return unique(join(...this.items.map(i => i.functions))); } | ||
toString() { return this.items.map(i => i.toString()).join(' '); } | ||
toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); } | ||
collapse() { return collapseTerm(this.items).collapse(); } | ||
} | ||
// ============================================================================= | ||
const PRECEDENCE = words('+ − * × · // ^'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -259,5 +482,6 @@ | ||
class ExprFunction { | ||
class ExprFunction extends ExprElement { | ||
constructor(fn, args=[]) { | ||
super(); | ||
this.fn = fn; | ||
@@ -273,3 +497,3 @@ this.args = args; | ||
case '+': return args.reduce((a, b) => a + b, 0); | ||
case '-': return (args.length > 1) ? args[1] - args[0] : -args[0]; | ||
case '−': return (args.length > 1) ? args[0] - args[1] : -args[0]; | ||
case '*': | ||
@@ -286,2 +510,3 @@ case '·': | ||
case 'root': return Math.pow(args[0], 1 / args[1]); | ||
case '(': return args[0]; | ||
// TODO Implement for all functions | ||
@@ -297,2 +522,6 @@ } | ||
collapse() { | ||
return new ExprFunction(this.fn, this.args.map(a => a.collapse())); | ||
} | ||
get simplified() { | ||
@@ -315,5 +544,7 @@ // TODO Write CAS simplification algorithms | ||
if (this.fn === '-') | ||
return args.length > 1 ? args.join(' – ') : '-' + args[0]; | ||
if (this.fn === '−') | ||
return args.length > 1 ? args.join(' − ') : '−' + args[0]; | ||
if (this.fn === '^') return args.join('^'); | ||
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn)) | ||
@@ -337,4 +568,4 @@ return args.join(' ' + this.fn + ' '); | ||
if (this.fn === '-') return args.length > 1 ? | ||
args.join('<mo value="-">–</mo>') : '<mo rspace="0" value="-">–</mo>' + args[0]; | ||
if (this.fn === '−') return args.length > 1 ? | ||
args.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + args[0]; | ||
@@ -345,3 +576,9 @@ if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥')) | ||
if (isOneOf(this.fn, '*', '×', '·')) { | ||
return args.join(''); | ||
let str = args[0]; | ||
for (let i = 1; i < args.length - 1; ++i) { | ||
// We only show the × symbol between consecutive numbers. | ||
const showTimes = (this.args[0] instanceof ExprNumber && this.args[1] instanceof ExprNumber); | ||
str += (showTimes ? `<mo value="×">×</mo>` : '') + args[1]; | ||
} | ||
return str; | ||
} | ||
@@ -473,4 +710,4 @@ | ||
function findBinaryFunction(tokens, fn, toFn) { | ||
if (isOperator(tokens[0], fn) || isOperator(tokens[tokens.length - 1], fn)) | ||
throw ExprError.startingOperator(fn); | ||
if (isOperator(tokens[0], fn)) throw ExprError.startOperator(tokens[0]); | ||
if (isOperator(last(tokens), fn)) throw ExprError.endOperator(last(tokens)); | ||
@@ -541,15 +778,66 @@ for (let i = 1; i < tokens.length - 1; ++i) { | ||
function findAssociativeFunction(tokens, symbol, implicit=false) { | ||
const result = []; | ||
let buffer = []; | ||
let lastWasSymbol = false; | ||
function clearBuffer() { | ||
if (!buffer.length) return; | ||
result.push(buffer.length > 1 ? new ExprFunction(symbol[0], buffer) : buffer[0]); | ||
buffer = []; | ||
} | ||
for (let t of tokens) { | ||
if (isOperator(t, symbol)) { | ||
if (lastWasSymbol || !buffer.length) throw ExprError.invalidExpression(); | ||
lastWasSymbol = true; | ||
} else if (t instanceof ExprOperator) { | ||
clearBuffer(); | ||
result.push(t); | ||
lastWasSymbol = false; | ||
} else { | ||
// If implicit=true, we allow implicit multiplication, except where the | ||
// second factor is a number. For example, "3 5" is invalid. | ||
const noImplicit = (!implicit || t instanceof ExprNumber); | ||
if (buffer.length && !lastWasSymbol && noImplicit) throw ExprError.invalidExpression(); | ||
buffer.push(t); | ||
lastWasSymbol = false; | ||
} | ||
} | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
clearBuffer(); | ||
return result; | ||
} | ||
function collapseTerm(tokens) { | ||
// Filter out whitespace. | ||
tokens = tokens.filter(t => !(t instanceof ExprSpace)); | ||
if (!tokens.length) throw ExprError.invalidExpression(); | ||
// Match percentage and factorial operators. | ||
if (isOperator(tokens[0], '%!')) throw ExprError.startOperator(tokens[0].o); | ||
for (let i = 0; i < tokens.length; ++i) { | ||
if (!isOperator(tokens[i], '%!')) continue; | ||
tokens.splice(i - 1, 2, new ExprFunction(tokens[i].o, [tokens[i - 1]])); | ||
i -= 1; | ||
} | ||
// Match comparison and division operators. | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '//', '/'); | ||
// TODO Match multiplication and implicit multiplication | ||
// Match multiplication operators. | ||
tokens = findAssociativeFunction(tokens, '* × ·', true); | ||
// TODO Match starting - or ± | ||
// Match - and ± operators. | ||
if (isOperator(tokens[0], '− ±')) { | ||
tokens.splice(0, 2, new ExprFunction(tokens[0].o, [tokens[1]])); | ||
} | ||
findBinaryFunction(tokens, '− ±'); | ||
findBinaryFunction(tokens, '-', '-'); | ||
findBinaryFunction(tokens, '±', '±'); | ||
// Match + operators. | ||
if (isOperator(tokens[0], '+')) tokens = tokens.slice(1); | ||
tokens = findAssociativeFunction(tokens, '+'); | ||
// TODO Match addition | ||
if (tokens.length > 1) throw ExprError.invalidExpression(); | ||
@@ -562,119 +850,43 @@ return tokens[0]; | ||
const CONSTANTS = { | ||
pi: Math.PI, | ||
e: Math.E | ||
}; | ||
/** | ||
* Maths Expression | ||
* Parses a string to an expression. | ||
* @param {string} str | ||
* @param {boolean} collapse | ||
* @returns {Expression} | ||
*/ | ||
class Expression { | ||
/** | ||
* Parses a string to an expression. | ||
* @param {string} str | ||
* @returns {Expression} | ||
*/ | ||
static parse(str) { return matchBrackets(tokenize(str)) } | ||
/** | ||
* Evaluates an expression using a given map of variables and functions. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {number|null} | ||
*/ | ||
evaluate(_vars={}) { return null; } | ||
/** | ||
* Substitutes a new expression for a variable. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {Expression} | ||
*/ | ||
substitute(_vars={}) { return this; } | ||
/** | ||
* Returns the simplest mathematically equivalent expression. | ||
* @returns {Expression} | ||
*/ | ||
get simplified() { return this; } | ||
/** | ||
* Returns a list of all variables used in the expression. | ||
* @returns {String[]} | ||
*/ | ||
get variables() { return []; } | ||
/** | ||
* Returns a list of all functions called by the expression. | ||
* @returns {String[]} | ||
*/ | ||
get functions() { return []; } | ||
/** | ||
* Converts the expression to a plain text string. | ||
* @returns {string} | ||
*/ | ||
toString() { return ''; } | ||
/** | ||
* Converts the expression to a MathML string. | ||
* @param {Object.<String, Function>=} _custom | ||
* @returns {string} | ||
*/ | ||
toMathML(_custom={}) { return ''; } | ||
function parse(str, collapse = false) { | ||
const expr = matchBrackets(tokenize(str)); | ||
return collapse ? expr.collapse() : expr; | ||
} | ||
// ----------------------------------------------------------------------------- | ||
/** | ||
* Checks numerically if two expressions are equal. Obviously this is not a | ||
* very robust solution, but much easier than the full CAS simplification. | ||
* @param {Expression} expr1 | ||
* @param {Expression} expr2 | ||
* @returns {boolean} | ||
*/ | ||
function numEquals(expr1, expr2) { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
class ExprNumber extends Expression { | ||
constructor(n) { super(); this.n = n; } | ||
evaluate() { return this.n; } | ||
toString() { return '' + this.n; } | ||
toMathML() { return `<mn>${this.n}</mn>`; } | ||
} | ||
class ExprIdentifier extends Expression { | ||
constructor(i) { super(); this.i = i; } | ||
evaluate(vars={}) { | ||
if (this.i in vars) return vars[this.i]; | ||
if (this.i in CONSTANTS) return CONSTANTS[this.i]; | ||
throw ExprError.undefinedVariable(this.i); | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (!nearlyEquals(a, b)) return false; | ||
} | ||
substitute(vars={}) { return vars[this.i] || this; } | ||
get variables() { return [this.i]; } | ||
toString() { return this.i; } | ||
toMathML() { return `<mi>${this.i}</mi>`; } | ||
return true; | ||
} | ||
class ExprString extends Expression { | ||
constructor(s) { super(); this.s = s; } | ||
evaluate() { throw ExprError.undefinedVariable(this.s); } | ||
toString() { return '"' + this.s + '"'; } | ||
toMathML() { return `<mtext>${this.s}</mtext>`; } | ||
} | ||
const Expression = { | ||
numEquals, | ||
parse: cache(parse) | ||
}; | ||
class ExprSpace { | ||
toString() { return ' '; } | ||
toMathML() { return `<mspace/>`; } | ||
} | ||
class ExprOperator { | ||
constructor(o) { this.o = o; } | ||
toString() { return this.o.replace('//', '/'); } | ||
toMathML() { return `<mo value="${this.toString()}">${this.toString()}</mo>`; } | ||
} | ||
class ExprTerm extends Expression { | ||
constructor(items) { super(); this.items = items; } | ||
evaluate(vars={}) { return this.toFunction().evaluate(vars); } | ||
substitute(vars={}) { return this.toFunction().substitute(vars); } | ||
get simplified() { return this.toFunction().variables; } | ||
get variables() { return this.toFunction().variables; } | ||
get functions() { return this.toFunction().functions; } | ||
toString() { return this.items.map(i => i.toString()).join(' '); } | ||
toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); } | ||
toFunction() { return collapseTerm(this.items); } | ||
} | ||
// ============================================================================= | ||
@@ -681,0 +893,0 @@ |
{ | ||
"name": "@mathigon/hilbert", | ||
"version": "0.1.1", | ||
"version": "0.2.1", | ||
"description": "JavaScript expression parsing, MathML rendering and CAS.", | ||
@@ -28,4 +28,4 @@ "keywords": [ | ||
"dependencies": { | ||
"@mathigon/fermat": "^0.2.5", | ||
"@mathigon/core": "^0.2.3" | ||
"@mathigon/fermat": "^0.2.6", | ||
"@mathigon/core": "^0.2.5" | ||
}, | ||
@@ -32,0 +32,0 @@ "devDependencies": { |
@@ -15,5 +15,3 @@ # Hilbert.js | ||
* [ ] __Finish collapseTerms: match non-binary addition and multiplication, | ||
match implicit multiplication, match starting - or ±.__ | ||
* [ ] Decide when to show the times symbol between consecutive factors. | ||
* [ ] _Remove expressions code from `fermat.js`. Update x-equation._ | ||
* [ ] Support for functions with subscripts (e.g. `log_a(b)`). | ||
@@ -25,6 +23,4 @@ * [ ] Support for super+subscripts (e.g. `a_n^2` or `a^2_n`). | ||
special functions. | ||
* [ ] Write CAS Expression simplification algorithms and `equals()`, | ||
`numEquals()` and `same()` methods. | ||
* [ ] Remove expressions code from `fermat.js`. | ||
* [ ] Write many more tests. | ||
* [ ] Write CAS Expression simplification algorithms, `equals()` and `same()` methods. | ||
* [ ] Write many more tests. Visual tests for MathML. | ||
@@ -31,0 +27,0 @@ |
@@ -46,6 +46,10 @@ // ============================================================================= | ||
static startingOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot start or end with a “${x}”.`); | ||
static startOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot start with a “${x}”.`); | ||
} | ||
static endOperator(x) { | ||
return new ExprError('SyntaxError', `A term cannot end with a “${x}”.`); | ||
} | ||
static consecutiveOperators(x, y) { | ||
@@ -52,0 +56,0 @@ return new ExprError('SyntaxError', `A “${x}” cannot be followed by a “${y}”.`); |
@@ -8,121 +8,47 @@ // ============================================================================= | ||
import { tokenize, matchBrackets, collapseTerm } from './parser' | ||
import { ExprError } from './errors' | ||
import { unique, cache } from '@mathigon/core'; | ||
import { nearlyEquals } from '@mathigon/fermat'; | ||
import { CONSTANTS } from './symbols' | ||
import { tokenize, matchBrackets } from './parser' | ||
const CONSTANTS = { | ||
pi: Math.PI, | ||
e: Math.E | ||
}; | ||
/** | ||
* Maths Expression | ||
* Parses a string to an expression. | ||
* @param {string} str | ||
* @param {boolean} collapse | ||
* @returns {Expression} | ||
*/ | ||
export class Expression { | ||
/** | ||
* Parses a string to an expression. | ||
* @param {string} str | ||
* @returns {Expression} | ||
*/ | ||
static parse(str) { return matchBrackets(tokenize(str)) } | ||
/** | ||
* Evaluates an expression using a given map of variables and functions. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {number|null} | ||
*/ | ||
evaluate(_vars={}) { return null; } | ||
/** | ||
* Substitutes a new expression for a variable. | ||
* @param {Object.<String, Expression>=} _vars | ||
* @returns {Expression} | ||
*/ | ||
substitute(_vars={}) { return this; } | ||
/** | ||
* Returns the simplest mathematically equivalent expression. | ||
* @returns {Expression} | ||
*/ | ||
get simplified() { return this; } | ||
/** | ||
* Returns a list of all variables used in the expression. | ||
* @returns {String[]} | ||
*/ | ||
get variables() { return []; } | ||
/** | ||
* Returns a list of all functions called by the expression. | ||
* @returns {String[]} | ||
*/ | ||
get functions() { return []; } | ||
/** | ||
* Converts the expression to a plain text string. | ||
* @returns {string} | ||
*/ | ||
toString() { return ''; } | ||
/** | ||
* Converts the expression to a MathML string. | ||
* @param {Object.<String, Function>=} _custom | ||
* @returns {string} | ||
*/ | ||
toMathML(_custom={}) { return ''; } | ||
function parse(str, collapse = false) { | ||
const expr = matchBrackets(tokenize(str)); | ||
return collapse ? expr.collapse() : expr; | ||
} | ||
// ----------------------------------------------------------------------------- | ||
/** | ||
* Checks numerically if two expressions are equal. Obviously this is not a | ||
* very robust solution, but much easier than the full CAS simplification. | ||
* @param {Expression} expr1 | ||
* @param {Expression} expr2 | ||
* @returns {boolean} | ||
*/ | ||
function numEquals(expr1, expr2) { | ||
const vars = unique([...expr1.variables, ...expr2.variables]); | ||
const fn1 = expr1.collapse(); | ||
const fn2 = expr2.collapse(); | ||
export class ExprNumber extends Expression { | ||
constructor(n) { super(); this.n = n; } | ||
evaluate() { return this.n; } | ||
toString() { return '' + this.n; } | ||
toMathML() { return `<mn>${this.n}</mn>`; } | ||
} | ||
export class ExprIdentifier extends Expression { | ||
constructor(i) { super(); this.i = i; } | ||
evaluate(vars={}) { | ||
if (this.i in vars) return vars[this.i]; | ||
if (this.i in CONSTANTS) return CONSTANTS[this.i]; | ||
throw ExprError.undefinedVariable(this.i); | ||
// We only test positive random numbers, because negative numbers raised | ||
// to non-integer powers return NaN. | ||
for (let i = 0; i < 5; ++i) { | ||
const substitution = {}; | ||
for (let v of vars) substitution[v] = CONSTANTS[v] || Math.random() * 5; | ||
const a = fn1.evaluate(substitution); | ||
const b = fn2.evaluate(substitution); | ||
if (!nearlyEquals(a, b)) return false; | ||
} | ||
substitute(vars={}) { return vars[this.i] || this; } | ||
get variables() { return [this.i]; } | ||
toString() { return this.i; } | ||
toMathML() { return `<mi>${this.i}</mi>`; } | ||
return true; | ||
} | ||
export class ExprString extends Expression { | ||
constructor(s) { super(); this.s = s; } | ||
evaluate() { throw ExprError.undefinedVariable(this.s); } | ||
toString() { return '"' + this.s + '"'; } | ||
toMathML() { return `<mtext>${this.s}</mtext>`; } | ||
} | ||
export class ExprSpace { | ||
toString() { return ' '; } | ||
toMathML() { return `<mspace/>`; } | ||
} | ||
export class ExprOperator { | ||
constructor(o) { this.o = o; } | ||
toString() { return this.o.replace('//', '/'); } | ||
toMathML() { return `<mo value="${this.toString()}">${this.toString()}</mo>`; } | ||
} | ||
export class ExprTerm extends Expression { | ||
constructor(items) { super(); this.items = items; } | ||
evaluate(vars={}) { return this.toFunction().evaluate(vars); } | ||
substitute(vars={}) { return this.toFunction().substitute(vars); } | ||
get simplified() { return this.toFunction().variables; } | ||
get variables() { return this.toFunction().variables; } | ||
get functions() { return this.toFunction().functions; } | ||
toString() { return this.items.map(i => i.toString()).join(' '); } | ||
toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); } | ||
toFunction() { return collapseTerm(this.items); } | ||
} | ||
export const Expression = { | ||
numEquals, | ||
parse: cache(parse) | ||
}; |
@@ -10,7 +10,7 @@ // ============================================================================= | ||
import { BRACKETS } from './symbols' | ||
import { ExprTerm } from './expression' | ||
import { ExprElement, ExprTerm, ExprNumber } from './elements' | ||
import { ExprError } from './errors' | ||
const PRECEDENCE = words('+ - * × · // ^'); | ||
const PRECEDENCE = words('+ − * × · // ^'); | ||
const COMMA = '<mo value="," lspace="0">,</mo>'; | ||
@@ -32,5 +32,6 @@ | ||
export class ExprFunction { | ||
export class ExprFunction extends ExprElement { | ||
constructor(fn, args=[]) { | ||
super(); | ||
this.fn = fn; | ||
@@ -46,3 +47,3 @@ this.args = args; | ||
case '+': return args.reduce((a, b) => a + b, 0); | ||
case '-': return (args.length > 1) ? args[1] - args[0] : -args[0]; | ||
case '−': return (args.length > 1) ? args[0] - args[1] : -args[0]; | ||
case '*': | ||
@@ -59,2 +60,3 @@ case '·': | ||
case 'root': return Math.pow(args[0], 1 / args[1]); | ||
case '(': return args[0]; | ||
// TODO Implement for all functions | ||
@@ -70,2 +72,6 @@ } | ||
collapse() { | ||
return new ExprFunction(this.fn, this.args.map(a => a.collapse())); | ||
} | ||
get simplified() { | ||
@@ -88,5 +94,7 @@ // TODO Write CAS simplification algorithms | ||
if (this.fn === '-') | ||
return args.length > 1 ? args.join(' – ') : '-' + args[0]; | ||
if (this.fn === '−') | ||
return args.length > 1 ? args.join(' − ') : '−' + args[0]; | ||
if (this.fn === '^') return args.join('^'); | ||
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn)) | ||
@@ -110,4 +118,4 @@ return args.join(' ' + this.fn + ' '); | ||
if (this.fn === '-') return args.length > 1 ? | ||
args.join('<mo value="-">–</mo>') : '<mo rspace="0" value="-">–</mo>' + args[0]; | ||
if (this.fn === '−') return args.length > 1 ? | ||
args.join('<mo value="−">−</mo>') : '<mo rspace="0" value="−">−</mo>' + args[0]; | ||
@@ -118,4 +126,9 @@ if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥')) | ||
if (isOneOf(this.fn, '*', '×', '·')) { | ||
const showTimes = false; // TODO Decide when to show times symbol. | ||
return args.join(showTimes ? `<mo value="×">×</mo>` : ''); | ||
let str = args[0]; | ||
for (let i = 1; i < args.length - 1; ++i) { | ||
// We only show the × symbol between consecutive numbers. | ||
const showTimes = (this.args[0] instanceof ExprNumber && this.args[1] instanceof ExprNumber); | ||
str += (showTimes ? `<mo value="×">×</mo>` : '') + args[1]; | ||
} | ||
return str; | ||
} | ||
@@ -122,0 +135,0 @@ |
@@ -10,3 +10,3 @@ // ============================================================================= | ||
import { SPECIAL_OPERATORS, SPECIAL_IDENTIFIERS, IDENTIFIER_SYMBOLS, OPERATOR_SYMBOLS, BRACKETS } from './symbols' | ||
import { ExprNumber, ExprIdentifier, ExprOperator, ExprSpace, ExprString, ExprTerm } from "./expression"; | ||
import { ExprNumber, ExprIdentifier, ExprOperator, ExprSpace, ExprString, ExprTerm } from "./elements"; | ||
import { ExprFunction } from "./functions"; | ||
@@ -118,4 +118,4 @@ import { ExprError } from "./errors"; | ||
function findBinaryFunction(tokens, fn, toFn) { | ||
if (isOperator(tokens[0], fn) || isOperator(tokens[tokens.length - 1], fn)) | ||
throw ExprError.startingOperator(fn); | ||
if (isOperator(tokens[0], fn)) throw ExprError.startOperator(tokens[0]); | ||
if (isOperator(last(tokens), fn)) throw ExprError.endOperator(last(tokens)); | ||
@@ -186,17 +186,68 @@ for (let i = 1; i < tokens.length - 1; ++i) { | ||
function findAssociativeFunction(tokens, symbol, implicit=false) { | ||
const result = []; | ||
let buffer = []; | ||
let lastWasSymbol = false; | ||
function clearBuffer() { | ||
if (!buffer.length) return; | ||
result.push(buffer.length > 1 ? new ExprFunction(symbol[0], buffer) : buffer[0]); | ||
buffer = []; | ||
} | ||
for (let t of tokens) { | ||
if (isOperator(t, symbol)) { | ||
if (lastWasSymbol || !buffer.length) throw ExprError.invalidExpression(); | ||
lastWasSymbol = true; | ||
} else if (t instanceof ExprOperator) { | ||
clearBuffer(); | ||
result.push(t); | ||
lastWasSymbol = false; | ||
} else { | ||
// If implicit=true, we allow implicit multiplication, except where the | ||
// second factor is a number. For example, "3 5" is invalid. | ||
const noImplicit = (!implicit || t instanceof ExprNumber); | ||
if (buffer.length && !lastWasSymbol && noImplicit) throw ExprError.invalidExpression(); | ||
buffer.push(t); | ||
lastWasSymbol = false; | ||
} | ||
} | ||
if (lastWasSymbol) throw ExprError.invalidExpression(); | ||
clearBuffer(); | ||
return result; | ||
} | ||
export function collapseTerm(tokens) { | ||
// Filter out whitespace. | ||
tokens = tokens.filter(t => !(t instanceof ExprSpace)); | ||
if (!tokens.length) throw ExprError.invalidExpression(); | ||
// Match percentage and factorial operators. | ||
if (isOperator(tokens[0], '%!')) throw ExprError.startOperator(tokens[0].o); | ||
for (let i = 0; i < tokens.length; ++i) { | ||
if (!isOperator(tokens[i], '%!')) continue; | ||
tokens.splice(i - 1, 2, new ExprFunction(tokens[i].o, [tokens[i - 1]])); | ||
i -= 1; | ||
} | ||
// Match comparison and division operators. | ||
findBinaryFunction(tokens, '= < > ≤ ≥'); | ||
findBinaryFunction(tokens, '//', '/'); | ||
// TODO Match multiplication and implicit multiplication | ||
// Match multiplication operators. | ||
tokens = findAssociativeFunction(tokens, '* × ·', true); | ||
// TODO Match starting - or ± | ||
// Match - and ± operators. | ||
if (isOperator(tokens[0], '− ±')) { | ||
tokens.splice(0, 2, new ExprFunction(tokens[0].o, [tokens[1]])); | ||
} | ||
findBinaryFunction(tokens, '− ±'); | ||
findBinaryFunction(tokens, '-', '-'); | ||
findBinaryFunction(tokens, '±', '±'); | ||
// Match + operators. | ||
if (isOperator(tokens[0], '+')) tokens = tokens.slice(1); | ||
tokens = findAssociativeFunction(tokens, '+'); | ||
// TODO Match addition | ||
if (tokens.length > 1) throw ExprError.invalidExpression(); | ||
return tokens[0]; | ||
} |
@@ -8,2 +8,8 @@ // ============================================================================= | ||
export const CONSTANTS = { | ||
pi: Math.PI, | ||
π: Math.PI, | ||
e: Math.E | ||
}; | ||
export const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'}; | ||
@@ -16,2 +22,4 @@ | ||
'+-': '±', | ||
'–': '−', | ||
'-': '−', | ||
xx: '×', | ||
@@ -98,4 +106,4 @@ sum: '∑', | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~^_…'; | ||
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–−~^_…'; | ||
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS); | ||
export const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS]; |
// ============================================================================= | ||
// Fermat.js | Expressions Tests | ||
// Hilbert.js | MathML Tests | ||
// (c) Mathigon | ||
@@ -22,5 +22,5 @@ // ============================================================================= | ||
test.equal(mathML('+'), '<mo value="+">+</mo>'); | ||
test.equal(mathML('-'), '<mo value="-">-</mo>'); | ||
test.equal(mathML('-'), '<mo value="−">−</mo>'); | ||
test.equal(mathML('1+1 = 2'), '<mn>1</mn><mo value="+">+</mo><mn>1</mn><mo value="=">=</mo><mn>2</mn>'); | ||
test.equal(mathML('3 - 2=1'), '<mn>3</mn><mo value="-">-</mo><mn>2</mn><mo value="=">=</mo><mn>1</mn>'); | ||
test.equal(mathML('3 - 2=1'), '<mn>3</mn><mo value="−">−</mo><mn>2</mn><mo value="=">=</mo><mn>1</mn>'); | ||
test.end(); | ||
@@ -38,6 +38,6 @@ }); | ||
test.equal(mathML('tan = sin/cos'), '<mi>tan</mi><mo value="=">=</mo><mfrac><mi>sin</mi><mi>cos</mi></mfrac>'); | ||
test.equal(mathML('sinh(x) = (e^x - e^(-x))/2'), '<mi>sinh</mi><mfenced><mi>x</mi></mfenced><mo value="=">=</mo><mfrac><mrow><msup><mi>e</mi><mi>x</mi></msup><mo value="-">-</mo><msup><mi>e</mi><mrow><mo value="-">-</mo><mi>x</mi></mrow></msup></mrow><mn>2</mn></mfrac>'); | ||
test.equal(mathML('sinh(x) = (e^x - e^(-x))/2'), '<mi>sinh</mi><mfenced><mi>x</mi></mfenced><mo value="=">=</mo><mfrac><mrow><msup><mi>e</mi><mi>x</mi></msup><mo value="−">−</mo><msup><mi>e</mi><mrow><mo value="−">−</mo><mi>x</mi></mrow></msup></mrow><mn>2</mn></mfrac>'); | ||
test.equal(mathML('ln(x^2) = 2 ln(x)'), '<mi>ln</mi><mfenced><msup><mi>x</mi><mn>2</mn></msup></mfenced><mo value="=">=</mo><mn>2</mn><mi>ln</mi><mfenced><mi>x</mi></mfenced>'); | ||
test.equal(mathML('ln(x/y) = ln(x) - ln(y)'), '<mi>ln</mi><mfenced><mfrac><mi>x</mi><mi>y</mi></mfrac></mfenced><mo value="=">=</mo><mi>ln</mi><mfenced><mi>x</mi></mfenced><mo value="-">-</mo><mi>ln</mi><mfenced><mi>y</mi></mfenced>'); | ||
test.equal(mathML('a^(p-1) == 1'), '<msup><mi>a</mi><mrow><mi>p</mi><mo value="-">-</mo><mn>1</mn></mrow></msup><mo value="≡">≡</mo><mn>1</mn>'); | ||
test.equal(mathML('ln(x/y) = ln(x) - ln(y)'), '<mi>ln</mi><mfenced><mfrac><mi>x</mi><mi>y</mi></mfrac></mfenced><mo value="=">=</mo><mi>ln</mi><mfenced><mi>x</mi></mfenced><mo value="−">−</mo><mi>ln</mi><mfenced><mi>y</mi></mfenced>'); | ||
test.equal(mathML('a^(p-1) == 1'), '<msup><mi>a</mi><mrow><mi>p</mi><mo value="−">−</mo><mn>1</mn></mrow></msup><mo value="≡">≡</mo><mn>1</mn>'); | ||
// test.equal(mathML('log_b(x) = log_k(x)/log_k(b)'), 'xxx'); // TODO Support functions with subscripts | ||
@@ -69,3 +69,3 @@ test.end(); | ||
test.equal(mathML('sqrt(2) ~~ 1.414213562'), '<msqrt><mn>2</mn></msqrt><mo value="≈">≈</mo><mn>1.414213562</mn>'); | ||
test.equal(mathML('x = (-b +- sqrt(b^2 - 4a c)) / (2a)'), '<mi>x</mi><mo value="=">=</mo><mfrac><mrow><mo value="-">-</mo><mi>b</mi><mo value="±">±</mo><msqrt><msup><mi>b</mi><mn>2</mn></msup><mo value="-">-</mo><mn>4</mn><mi>a</mi><mi>c</mi></msqrt></mrow><mrow><mn>2</mn><mi>a</mi></mrow></mfrac>'); | ||
test.equal(mathML('x = (-b +- sqrt(b^2 - 4a c)) / (2a)'), '<mi>x</mi><mo value="=">=</mo><mfrac><mrow><mo value="−">−</mo><mi>b</mi><mo value="±">±</mo><msqrt><msup><mi>b</mi><mn>2</mn></msup><mo value="−">−</mo><mn>4</mn><mi>a</mi><mi>c</mi></msqrt></mrow><mrow><mn>2</mn><mi>a</mi></mrow></mfrac>'); | ||
test.equal(mathML('phi = (1 + sqrt(5))/2'), '<mi>φ</mi><mo value="=">=</mo><mfrac><mrow><mn>1</mn><mo value="+">+</mo><msqrt><mn>5</mn></msqrt></mrow><mn>2</mn></mfrac>'); | ||
@@ -80,6 +80,6 @@ test.equal(mathML('sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + …)))))))'), '<msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><msqrt><mn>1</mn><mo value="+">+</mo><mo value="…">…</mo></msqrt></msqrt></msqrt></msqrt></msqrt></msqrt></msqrt>'); | ||
test.equal(mathML('(a,b,c)'), '<mfenced open="(" close=")"><mi>a</mi><mo value="," lspace="0">,</mo><mi>b</mi><mo value="," lspace="0">,</mo><mi>c</mi></mfenced>'); | ||
test.equal(mathML('(x+y)(x-y) = x^2-y^2'), '<mfenced open="(" close=")"><mi>x</mi><mo value="+">+</mo><mi>y</mi></mfenced><mfenced open="(" close=")"><mi>x</mi><mo value="-">-</mo><mi>y</mi></mfenced><mo value="=">=</mo><msup><mi>x</mi><mn>2</mn></msup><mo value="-">-</mo><msup><mi>y</mi><mn>2</mn></msup>'); | ||
test.equal(mathML('e^(-x)'), '<msup><mi>e</mi><mrow><mo value="-">-</mo><mi>x</mi></mrow></msup>'); | ||
test.equal(mathML('(x+y)(x-y) = x^2-y^2'), '<mfenced open="(" close=")"><mi>x</mi><mo value="+">+</mo><mi>y</mi></mfenced><mfenced open="(" close=")"><mi>x</mi><mo value="−">−</mo><mi>y</mi></mfenced><mo value="=">=</mo><msup><mi>x</mi><mn>2</mn></msup><mo value="−">−</mo><msup><mi>y</mi><mn>2</mn></msup>'); | ||
test.equal(mathML('e^(-x)'), '<msup><mi>e</mi><mrow><mo value="−">−</mo><mi>x</mi></mrow></msup>'); | ||
test.equal(mathML('e^(i tau) = 1'), '<msup><mi>e</mi><mrow><mi>i</mi><mi>τ</mi></mrow></msup><mo value="=">=</mo><mn>1</mn>'); | ||
test.end(); | ||
}); |
// ============================================================================= | ||
// Fermat.js | Expressions Tests | ||
// Hilbert.js | Parsing Tests | ||
// (c) Mathigon | ||
@@ -17,3 +17,3 @@ // ============================================================================= | ||
test.equal(str('1'), '1'); | ||
test.equal(str('-1'), '- 1'); | ||
test.equal(str('-1'), '− 1'); | ||
test.equal(expr('x + y').toString(), 'x + y'); | ||
@@ -25,1 +25,8 @@ test.equal(expr('aa + bb + cc').toString(), 'aa + bb + cc'); | ||
}); | ||
tape('special operators', function(test) { | ||
test.equal(expr('x - y').toString(), 'x − y'); | ||
test.equal(expr('x – y').toString(), 'x − y'); | ||
test.equal(expr('x − y').toString(), 'x − y'); | ||
test.end(); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
59450
17
1449
62
Updated@mathigon/core@^0.2.5
Updated@mathigon/fermat@^0.2.6