Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@mathigon/hilbert

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mathigon/hilbert - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

src/errors.js

413

build/hilbert.js

@@ -6,3 +6,59 @@ 'use strict';

// =============================================================================
// Hilbert.js | Expression Errors
// (c) Mathigon
// =============================================================================
/**
* Expression Error Class
*/
class ExprError extends Error {
constructor(name, message) {
super(message);
this.name = name;
}
// ---------------------------------------------------------------------------
// Eval Errors
static undefinedVariable(x) {
return new ExprError('EvalError', `Undefined variable “${x}”.`);
}
static undefinedFunction(x) {
return new ExprError('EvalError', `Undefined function “${x}”.`);
}
// ---------------------------------------------------------------------------
// Syntax Errors
static invalidCharacter(x) {
return new ExprError('SyntaxError', `Unknown symbol “${x}”.`);
}
static conflictingBrackets(x) {
return new ExprError('SyntaxError', `Conflicting brackets “${x}”.`);
}
static unclosedBracket(x) {
return new ExprError('SyntaxError', `Unclosed bracket “${x}”.`);
}
static startingOperator(x) {
return new ExprError('SyntaxError', `A term cannot start or end with a “${x}”.`);
}
static consecutiveOperators(x, y) {
return new ExprError('SyntaxError', `A “${x}” cannot be followed by a “${y}”.`);
}
static invalidExpression() {
return new ExprError('SyntaxError', `This expression is invalid.`);
}
}
// =============================================================================

@@ -12,2 +68,18 @@

/**
* Checks if x is strictly equal to any one of the following arguments
* @param {*} x
* @param {...*} values
* @returns {boolean}
*/
function isOneOf(x, ...values) {
for (let v of values) {
if (x === v) return true;
}
return false;
}
// =============================================================================
/**
* Returns the last item in an array, or the ith item from the end.

@@ -47,21 +119,14 @@ * @param {Array} array

// =============================================================================
/*
* Checks if an object is a string.
* @param {*} x
* @returns {boolean}
/**
* Splits a string into space separated words.
* @param {string} str
* @returns {Array<string>}
*/
function isString(x) {
return (x instanceof String) || (typeof x === 'string');
function words(str) {
if (!str) return [];
return str.trim().split(/\s+/);
}
/*
* Checks if an object is a number.
* @param {*} x
* @returns {boolean}
*/
function isNumber(x) {
return (x instanceof Number) || (typeof x === 'number');
}
// =============================================================================

@@ -77,6 +142,8 @@ // =============================================================================

const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'};
const SPECIAL_OPERATORS = {
'*': '·',
'**': '∗',
'//': '/',
'//': '//',
'+-': '±',

@@ -164,3 +231,3 @@ xx: '×',

const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~';
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~^_…';
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS);

@@ -172,9 +239,19 @@ const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS];

/* const PRECEDENCE = ['+', '-', '*', '/', 'sqrt', '^'];
const PRECEDENCE = words('+ - * × · // ^');
const COMMA = '<mo value="," lspace="0">,</mo>';
function needsBrackets(expr, parentOperator) {
if (isNumber(expr) || isString(expr)) return false;
return PRECEDENCE.indexOf(parentOperator) < PRECEDENCE.indexOf(expr[0]);
} */
function needsBrackets(expr, parentFn) {
if (!PRECEDENCE.includes(parentFn)) return false;
if (expr instanceof ExprTerm) return true;
if (!(expr instanceof ExprFunction)) return false;
if (!PRECEDENCE.includes(expr.fn)) return false;
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr);
}
function addRow(expr, string) {
const needsRow = (expr instanceof ExprTerm) || (expr instanceof ExprFunction);
return needsRow ? `<mrow>${string}</mrow>` : string;
}
class ExprFunction {

@@ -187,4 +264,3 @@

evaluate(vars) {
// TODO Implement for all functions
evaluate(vars={}) {
const args = this.args.map(a => a.evaluate(vars));

@@ -195,4 +271,6 @@ if (this.fn in vars) return vars[this.fn](...args);

case '+': return args.reduce((a, b) => a + b, 0);
// TODO case '-': return (a, b) => (b === undefined) ? -a : a - b;
case '*': return args.reduce((a, b) => a * b, 1);
case '-': return (args.length > 1) ? args[1] - args[0] : -args[0];
case '*':
case '·':
case '×': return args.reduce((a, b) => a * b, 1);
case '/': return args[0] / args[1];

@@ -204,8 +282,11 @@ case 'sin': return Math.sin(args[0]);

case 'sup': return Math.pow(args[0], args[1]);
case 'sqrt': return Math.sqrt(args[0]);
case 'root': return Math.pow(args[0], 1 / args[1]);
// TODO Implement for all functions
}
throw new ExprError('EvalError', `Unable to evaluate function "${this.fn}".`);
throw ExprError.undefinedFunction(this.fn);
}
substitute(vars) {
substitute(vars={}) {
return new ExprFunction(this.fn, this.args.map(a => a.substitute(vars)));

@@ -228,43 +309,52 @@ }

toString() {
// TODO Implement for all functions
const args = this.args.map(a => a.toString());
const args = this.args.map(a => needsBrackets(a, this.fn) ?
'(' + a.toString() + ')' : a.toString());
switch(this.fn) {
case '+': return args.join(' + ');
// '-': (a, b) => (b === undefined) ? '-' + a : a + ' - ' + b,
// '*': (...args) => args.join('*'),
// '/': (a, b) => a + '/' + b,
// '!': x => x + '!',
// '%': x => x + '%',
// 'abs': x => '|' + x + '|',
// '^': (a, b) => a + '^' + b,
// '=': (a, b) => a + ' = ' + b,
// '<': (a, b) => a + ' < ' + b,
// '>': (a, b) => a + ' > ' + b,
// '≤': (a, b) => a + ' ≤ ' + b,
// '≥': (a, b) => a + ' ≥ ' + b
}
if (this.fn === '-')
return args.length > 1 ? args.join(' – ') : '-' + args[0];
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn))
return args.join(' ' + this.fn + ' ');
if (isOneOf(this.fn, '(', '[', '{'))
return this.fn + this.args.join(', ') + BRACKETS[this.fn];
if (isOneOf(this.fn, '!', '%')) return args[0] + this.fn;
// TODO Implement other functions
return `${this.fn}(${args.join(', ')})`;
}
toMathML() {
// TODO Implement for all functions
// TODO Distinguish between fractions and '÷'.
const args = this.args.map(a => a.toMathML());
toMathML(custom={}) {
const args = this.args.map(a => needsBrackets(a, this.fn) ?
'<mfenced>' + a.toMathML() + '</mfenced>' : a.toMathML());
// const argsStr = this.args.map(a => needsBrackets(a, this.fn) ?
// `<mfenced>${a.toMathML()}</mfenced>` : a.toMathML());
if (this.fn in custom) return custom[this.fn](...args);
switch(this.fn) {
case 'sqrt': return `<msqrt>${args[0]}</msqrt>`;
case '/': return `<mfrac><mrow>${args[0]}</mrow><mrow>${args[1]}</mrow></mfrac>`;
case 'sup': return `<msup>${args[0]}<mrow>${args[1]}</mrow></msup>`;
case '*':
if (isNumber(this.args[0]) && isString(this.args[1])) return args.join('');
return args.join('<mo value="×">×</mo>');
case '+': return args.join('<mo value="+">+</mo>');
case '-': return args.join('<mo value="–">–</mo>');
default: return `<mi>TODO</mi>`;
if (this.fn === '-') return args.length > 1 ?
args.join('<mo value="-">–</mo>') : '<mo rspace="0" value="-">–</mo>' + args[0];
if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥'))
return args.join(`<mo value="${this.fn}">${this.fn}</mo>`);
if (isOneOf(this.fn, '*', '×', '·')) {
return args.join('');
}
if (this.fn === 'sqrt') return `<msqrt>${args[0]}</msqrt>`;
if (isOneOf(this.fn, '/', 'sup', 'sub', 'root')) {
const el = {'/': 'mfrac', 'sup': 'msup', 'sub': 'msub', 'root': 'mroot'}[this.fn];
const args1 = args.map((a, i) => addRow(this.args[i], a));
return `<${el}>${args1.join('')}</${el}>`;
}
if (isOneOf(this.fn, '(', '[', '{'))
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${args.join(COMMA)}</mfenced>`;
if (isOneOf(this.fn, '!', '%'))
return args[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`;
// TODO Implement other functions
return `<mi>${this.fn}</mi><mfenced>${args.join(COMMA)}</mfenced>`;
}

@@ -275,5 +365,6 @@ }

const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'};
// -----------------------------------------------------------------------------
// Tokenizer

@@ -284,3 +375,3 @@ function createToken(buffer, type) {

if (type === 'NUM') return new ExprNumber(+buffer);
if (type === 'SPACE' && buffer.length > 1) return [new ExprSpace()];
if (type === 'SPACE' && buffer.length > 1) return new ExprSpace();
if (type === 'STRING') return new ExprString(buffer);

@@ -329,3 +420,3 @@

OPERATOR_SYMBOLS.includes(s) ? 'OP' : s.match(/\s/) ? 'SPACE' : '';
if (!sType) throw new ExprError('Syntax Error', `Unknown symbol "${s}".`);
if (!sType) throw ExprError.invalidCharacter(s);

@@ -351,3 +442,5 @@ if (!type || (type === 'NUM' && sType !== 'NUM') ||

// -----------------------------------------------------------------------------
// Utility Functions

@@ -361,3 +454,3 @@ function makeTerm(items) {

for (let i of items) {
if (check[i]) {
if (check(i)) {
result.push([]);

@@ -371,3 +464,41 @@ } else {

function isOperator(expr, fns) {
return expr instanceof ExprOperator && words(fns).includes(expr.o);
}
function removeBrackets(expr) {
return (expr instanceof ExprFunction && expr.fn === '(') ? expr.args[0] : expr;
}
function findBinaryFunction(tokens, fn, toFn) {
if (isOperator(tokens[0], fn) || isOperator(tokens[tokens.length - 1], fn))
throw ExprError.startingOperator(fn);
for (let i = 1; i < tokens.length - 1; ++i) {
if (!isOperator(tokens[i], fn)) continue;
const a = tokens[i - 1];
const b = tokens[i + 1];
if (a instanceof ExprOperator) throw ExprError.consecutiveOperators(a.o, tokens[i].o);
if (b instanceof ExprOperator) throw ExprError.consecutiveOperators(tokens[i].o, b.o);
const args = [removeBrackets(a), removeBrackets(b)];
tokens.splice(i - 1, 3, new ExprFunction(toFn || tokens[i].o, args));
i -= 2;
}
}
// -----------------------------------------------------------------------------
// Match Brackets
function prepareTerm(tokens) {
// TODO Combine sup and sub calls into a single supsub function.
findBinaryFunction(tokens, '^', 'sup');
findBinaryFunction(tokens, '/');
return makeTerm(tokens);
}
function matchBrackets(tokens) {
findBinaryFunction(tokens, '_', 'sub');
const stack = [[]];

@@ -378,6 +509,6 @@

if (')]}'.includes(t.o) || (t.o === '|' && lastOpen === '|')) {
if (isOperator(t, ') ] }') || (isOperator(t, '|') && lastOpen === '|')) {
if (t.o !== BRACKETS[lastOpen])
throw new ExprError('SyntaxError', `Unmatched bracket “${t.o}”.`);
if (!isOperator(t, BRACKETS[lastOpen]))
throw ExprError.conflictingBrackets(t.o);

@@ -388,10 +519,10 @@ const closed = stack.pop();

// Check if this is a normal bracket, or a function call.
const isFn = (t.o === ')' && last(term) instanceof ExprIdentifier);
const fnName = isFn ? term.pop().i : t.o === '|' ? 'abs' : closed[0];
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier);
const fnName = isFn ? term.pop().i : isOperator(t, '|') ? 'abs' : closed[0].o;
// Support multiple arguments for function calls.
const args = splitArray(closed.slice(1), a => a.o === ',');
term.push(new ExprFunction(fnName, args.map(makeTerm)));
const args = splitArray(closed.slice(1), a => isOperator(a, ','));
term.push(new ExprFunction(fnName, args.map(prepareTerm)));
} else if('([{|'.includes(t.o)) {
} else if (isOperator(t, '( [ { |')) {
stack.push([t]);

@@ -404,51 +535,25 @@

if (stack.length > 1)
throw new ExprError('SyntaxError', `Unclosed bracket “${last(stack)[0]}”.`);
return makeTerm(stack[0]);
if (stack.length > 1) throw ExprError.unclosedBracket(last(stack)[0].o);
return prepareTerm(stack[0]);
}
// -----------------------------------------------------------------------------
// Collapse term items
/* function findBinaryFunction(tokens, chars) {
for (let i=0; i<tokens.length; ++i) {
if (chars.includes(tokens[i])) {
let a = tokens[i-1];
let b = tokens[i+1];
if (b == null) throw new ExprError('SyntaxError', `An expression cannot end with a “${tokens[i]}”.`);
if ('+-* / ^% ! ' .includes(a)) throw new ExprError('SyntaxError', `A “${a}” cannot be followed by a “${tokens[i]}”.`);
if ('+-* /^%!'.includes(b)) throw new ExprError('SyntaxError', `A “${tokens[i]}” cannot be followed by a “${b}”.`);
tokens.splice(i - 1, 3, [tokens[i], a, b]);
i -= 2;
}
}
}*/
function collapseTerm(tokens) {
findBinaryFunction(tokens, '= < > ≤ ≥');
findBinaryFunction(tokens, '//', '/');
function collapseTerm() {
// TODO Operators to functions, implicit multiplication, equals sign, ...
// TODO Support >2 arguments for + and *
// TODO Match multiplication and implicit multiplication
/*
findBinaryFunction(tokens, '^'); // Powers
findBinaryFunction(tokens, '* /'); // Multiplication and division.
// TODO Match starting - or ±
// Implicit multiplication (consecutive expressions)
for (let i=0; i<tokens.length-1; ++i) {
if (!'+-* /^%!'.includes(tokens[i]) && !'+-* /^%!'.includes(tokens[i+1])) {
tokens.splice(i, 2, ['*', tokens[i], tokens[i+1]]);
i -= 1;
}
}
findBinaryFunction(tokens, '-', '-');
findBinaryFunction(tokens, '±', '±');
// Leading (plus)minus.
if ('-±'.includes(tokens[0])) {
if (tokens.length <= 1) throw new ExprError('SyntaxError', `This expression is invalid.`);
tokens.splice(0, 2, [tokens[0], tokens[1]]);
}
// TODO Match addition
findBinaryFunction(tokens, '+-±'); // Addition and subtraction.
findBinaryFunction(tokens, '=<>≤≥'); // Equalities and inequalities.
if (tokens.length > 1) throw new ExprError('SyntaxError', `This expression is invalid.`);
return tokens[0]; */
if (tokens.length > 1) throw ExprError.invalidExpression();
return tokens[0];
}

@@ -464,20 +569,62 @@

/**
* Maths Expression
*/
class Expression {
/**
* Parses a string to an expression.
* @param {string} str
* @returns {Expression}
*/
static parse(str) { return matchBrackets(tokenize(str)) }
evaluate() { return null; }
substitute() { return this; }
/**
* 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 ''; }
toMathML() { return ''; }
}
class ExprError extends Error {
constructor(name, message) {
super(message);
this.name = name;
}
/**
* Converts the expression to a MathML string.
* @param {Object.<String, Function>=} _custom
* @returns {string}
*/
toMathML(_custom={}) { return ''; }
}
// -----------------------------------------------------------------------------
class ExprNumber extends Expression {

@@ -493,11 +640,11 @@ constructor(n) { super(); this.n = n; }

evaluate(vars) {
evaluate(vars={}) {
if (this.i in vars) return vars[this.i];
if (this.i in CONSTANTS) return CONSTANTS[this.i];
throw new ExprError('EvalError', `Unknown identifier "${this.i}".`);
throw ExprError.undefinedVariable(this.i);
}
substitute(vars) { return vars[this.i] || this; }
substitute(vars={}) { return vars[this.i] || this; }
get variables() { return [this.i]; }
toString() { return '' + this.i; }
toString() { return this.i; }
toMathML() { return `<mi>${this.i}</mi>`; }

@@ -508,3 +655,3 @@ }

constructor(s) { super(); this.s = s; }
evaluate() { throw new ExprError('EvalError', 'Expressions contains a string.'); }
evaluate() { throw ExprError.undefinedVariable(this.s); }
toString() { return '"' + this.s + '"'; }

@@ -516,3 +663,3 @@ toMathML() { return `<mtext>${this.s}</mtext>`; }

toString() { return ' '; }
toMathML() { return `<mspace></mspace>`; }
toMathML() { return `<mspace/>`; }
}

@@ -522,4 +669,4 @@

constructor(o) { this.o = o; }
toString() { return '' + this.o; }
toMathML() { return `<mo value="${this.o}">${this.o}</mo>`; }
toString() { return this.o.replace('//', '/'); }
toMathML() { return `<mo value="${this.toString()}">${this.toString()}</mo>`; }
}

@@ -529,4 +676,4 @@

constructor(items) { super(); this.items = items; }
evaluate(vars) { return this.toFunction().evaluate(vars); }
substitute(vars) { return this.toFunction().substitute(vars); }
evaluate(vars={}) { return this.toFunction().evaluate(vars); }
substitute(vars={}) { return this.toFunction().substitute(vars); }
get simplified() { return this.toFunction().variables; }

@@ -536,3 +683,3 @@ get variables() { return this.toFunction().variables; }

toString() { return this.items.map(i => i.toString()).join(' '); }
toMathML() { return this.items.map(i => i.toMathML()).join(''); }
toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); }
toFunction() { return collapseTerm(this.items); }

@@ -539,0 +686,0 @@ }

@@ -8,2 +8,3 @@ // =============================================================================

export {ExprError, Expression} from './src/expression'
export { ExprError } from './src/errors'
export { Expression } from './src/expression'
{
"name": "@mathigon/hilbert",
"version": "0.1.0",
"version": "0.1.1",
"description": "JavaScript expression parsing, MathML rendering and CAS.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -15,8 +15,15 @@ # Hilbert.js

* [ ] Write `collapseTerm()` function
* [ ] Write Function `evaluate()`, `toString()` and `toMathML()` methods
* [ ] Lots of testing
* [ ] Write Expression simplification algorithms, `equals()`, `numEquals()` and
`same()` methods
* [ ] Remove expressions code from `fermat.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.
* [ ] Support for functions with subscripts (e.g. `log_a(b)`).
* [ ] Support for super+subscripts (e.g. `a_n^2` or `a^2_n`).
* [ ] Support for large operators (sum, product and integral).
* [ ] Parse ^ operator from right to left (e.g. `2^2^2 == 2^(2^2)`).
* [ ] Add `evaluate()`, `toString()` and `toMathML()` methods for many more
special functions.
* [ ] Write CAS Expression simplification algorithms and `equals()`,
`numEquals()` and `same()` methods.
* [ ] Remove expressions code from `fermat.js`.
* [ ] Write many more tests.

@@ -23,0 +30,0 @@

@@ -9,2 +9,3 @@ // =============================================================================

import { tokenize, matchBrackets, collapseTerm } from './parser'
import { ExprError } from './errors'

@@ -17,20 +18,62 @@

/**
* Maths Expression
*/
export class Expression {
/**
* Parses a string to an expression.
* @param {string} str
* @returns {Expression}
*/
static parse(str) { return matchBrackets(tokenize(str)) }
evaluate() { return null; }
substitute() { return this; }
/**
* 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 ''; }
toMathML() { return ''; }
}
export class ExprError extends Error {
constructor(name, message) {
super(message);
this.name = name;
}
/**
* Converts the expression to a MathML string.
* @param {Object.<String, Function>=} _custom
* @returns {string}
*/
toMathML(_custom={}) { return ''; }
}
// -----------------------------------------------------------------------------
export class ExprNumber extends Expression {

@@ -46,11 +89,11 @@ constructor(n) { super(); this.n = n; }

evaluate(vars) {
evaluate(vars={}) {
if (this.i in vars) return vars[this.i];
if (this.i in CONSTANTS) return CONSTANTS[this.i];
throw new ExprError('EvalError', `Unknown identifier "${this.i}".`);
throw ExprError.undefinedVariable(this.i);
}
substitute(vars) { return vars[this.i] || this; }
substitute(vars={}) { return vars[this.i] || this; }
get variables() { return [this.i]; }
toString() { return '' + this.i; }
toString() { return this.i; }
toMathML() { return `<mi>${this.i}</mi>`; }

@@ -61,3 +104,3 @@ }

constructor(s) { super(); this.s = s; }
evaluate() { throw new ExprError('EvalError', 'Expressions contains a string.'); }
evaluate() { throw ExprError.undefinedVariable(this.s); }
toString() { return '"' + this.s + '"'; }

@@ -69,3 +112,3 @@ toMathML() { return `<mtext>${this.s}</mtext>`; }

toString() { return ' '; }
toMathML() { return `<mspace></mspace>`; }
toMathML() { return `<mspace/>`; }
}

@@ -75,4 +118,4 @@

constructor(o) { this.o = o; }
toString() { return '' + this.o; }
toMathML() { return `<mo value="${this.o}">${this.o}</mo>`; }
toString() { return this.o.replace('//', '/'); }
toMathML() { return `<mo value="${this.toString()}">${this.toString()}</mo>`; }
}

@@ -82,4 +125,4 @@

constructor(items) { super(); this.items = items; }
evaluate(vars) { return this.toFunction().evaluate(vars); }
substitute(vars) { return this.toFunction().substitute(vars); }
evaluate(vars={}) { return this.toFunction().evaluate(vars); }
substitute(vars={}) { return this.toFunction().substitute(vars); }
get simplified() { return this.toFunction().variables; }

@@ -89,4 +132,4 @@ get variables() { return this.toFunction().variables; }

toString() { return this.items.map(i => i.toString()).join(' '); }
toMathML() { return this.items.map(i => i.toMathML()).join(''); }
toMathML(custom={}) { return this.items.map(i => i.toMathML(custom)).join(''); }
toFunction() { return collapseTerm(this.items); }
}
// =============================================================================
// Hilbert.js | Fnctions
// Hilbert.js | Functions
// (c) Mathigon

@@ -8,13 +8,25 @@ // =============================================================================

import { unique, flatten, isNumber, isString } from '@mathigon/core'
import { ExprError } from './expression'
import { unique, flatten, words, isOneOf } from '@mathigon/core'
import { BRACKETS } from './symbols'
import { ExprTerm } from './expression'
import { ExprError } from './errors'
/* const PRECEDENCE = ['+', '-', '*', '/', 'sqrt', '^'];
const PRECEDENCE = words('+ - * × · // ^');
const COMMA = '<mo value="," lspace="0">,</mo>';
function needsBrackets(expr, parentOperator) {
if (isNumber(expr) || isString(expr)) return false;
return PRECEDENCE.indexOf(parentOperator) < PRECEDENCE.indexOf(expr[0]);
} */
function needsBrackets(expr, parentFn) {
if (!PRECEDENCE.includes(parentFn)) return false;
if (expr instanceof ExprTerm) return true;
if (!(expr instanceof ExprFunction)) return false;
if (!PRECEDENCE.includes(expr.fn)) return false;
return PRECEDENCE.indexOf(parentFn) > PRECEDENCE.indexOf(expr);
}
function addRow(expr, string) {
const needsRow = (expr instanceof ExprTerm) || (expr instanceof ExprFunction);
return needsRow ? `<mrow>${string}</mrow>` : string;
}
export class ExprFunction {

@@ -27,4 +39,3 @@

evaluate(vars) {
// TODO Implement for all functions
evaluate(vars={}) {
const args = this.args.map(a => a.evaluate(vars));

@@ -35,4 +46,6 @@ if (this.fn in vars) return vars[this.fn](...args);

case '+': return args.reduce((a, b) => a + b, 0);
// TODO case '-': return (a, b) => (b === undefined) ? -a : a - b;
case '*': return args.reduce((a, b) => a * b, 1);
case '-': return (args.length > 1) ? args[1] - args[0] : -args[0];
case '*':
case '·':
case '×': return args.reduce((a, b) => a * b, 1);
case '/': return args[0] / args[1];

@@ -44,8 +57,11 @@ case 'sin': return Math.sin(args[0]);

case 'sup': return Math.pow(args[0], args[1]);
case 'sqrt': return Math.sqrt(args[0]);
case 'root': return Math.pow(args[0], 1 / args[1]);
// TODO Implement for all functions
}
throw new ExprError('EvalError', `Unable to evaluate function "${this.fn}".`);
throw ExprError.undefinedFunction(this.fn);
}
substitute(vars) {
substitute(vars={}) {
return new ExprFunction(this.fn, this.args.map(a => a.substitute(vars)));

@@ -68,44 +84,54 @@ }

toString() {
// TODO Implement for all functions
const args = this.args.map(a => a.toString());
const args = this.args.map(a => needsBrackets(a, this.fn) ?
'(' + a.toString() + ')' : a.toString());
switch(this.fn) {
case '+': return args.join(' + ');
// '-': (a, b) => (b === undefined) ? '-' + a : a + ' - ' + b,
// '*': (...args) => args.join('*'),
// '/': (a, b) => a + '/' + b,
// '!': x => x + '!',
// '%': x => x + '%',
// 'abs': x => '|' + x + '|',
// '^': (a, b) => a + '^' + b,
// '=': (a, b) => a + ' = ' + b,
// '<': (a, b) => a + ' < ' + b,
// '>': (a, b) => a + ' > ' + b,
// '≤': (a, b) => a + ' ≤ ' + b,
// '≥': (a, b) => a + ' ≥ ' + b
}
if (this.fn === '-')
return args.length > 1 ? args.join(' – ') : '-' + args[0];
if (words('+ * × · / sup = < > ≤ ≥').includes(this.fn))
return args.join(' ' + this.fn + ' ');
if (isOneOf(this.fn, '(', '[', '{'))
return this.fn + this.args.join(', ') + BRACKETS[this.fn];
if (isOneOf(this.fn, '!', '%')) return args[0] + this.fn;
// TODO Implement other functions
return `${this.fn}(${args.join(', ')})`;
}
toMathML() {
// TODO Implement for all functions
// TODO Distinguish between fractions and '÷'.
const args = this.args.map(a => a.toMathML());
toMathML(custom={}) {
const args = this.args.map(a => needsBrackets(a, this.fn) ?
'<mfenced>' + a.toMathML() + '</mfenced>' : a.toMathML());
// const argsStr = this.args.map(a => needsBrackets(a, this.fn) ?
// `<mfenced>${a.toMathML()}</mfenced>` : a.toMathML());
if (this.fn in custom) return custom[this.fn](...args);
switch(this.fn) {
case 'sqrt': return `<msqrt>${args[0]}</msqrt>`;
case '/': return `<mfrac><mrow>${args[0]}</mrow><mrow>${args[1]}</mrow></mfrac>`;
case 'sup': return `<msup>${args[0]}<mrow>${args[1]}</mrow></msup>`;
case '*':
if (isNumber(this.args[0]) && isString(this.args[1])) return args.join('');
return args.join('<mo value="×">×</mo>');
case '+': return args.join('<mo value="+">+</mo>');
case '-': return args.join('<mo value="–">–</mo>');
default: return `<mi>TODO</mi>`;
if (this.fn === '-') return args.length > 1 ?
args.join('<mo value="-">–</mo>') : '<mo rspace="0" value="-">–</mo>' + args[0];
if (isOneOf(this.fn, '+', '=', '<', '>', '≤', '≥'))
return args.join(`<mo value="${this.fn}">${this.fn}</mo>`);
if (isOneOf(this.fn, '*', '×', '·')) {
const showTimes = false; // TODO Decide when to show times symbol.
return args.join(showTimes ? `<mo value="×">×</mo>` : '');
}
if (this.fn === 'sqrt') return `<msqrt>${args[0]}</msqrt>`;
if (isOneOf(this.fn, '/', 'sup', 'sub', 'root')) {
const el = {'/': 'mfrac', 'sup': 'msup', 'sub': 'msub', 'root': 'mroot'}[this.fn];
const args1 = args.map((a, i) => addRow(this.args[i], a));
return `<${el}>${args1.join('')}</${el}>`;
}
if (isOneOf(this.fn, '(', '[', '{'))
return `<mfenced open="${this.fn}" close="${BRACKETS[this.fn]}">${args.join(COMMA)}</mfenced>`;
if (isOneOf(this.fn, '!', '%'))
return args[0] + `<mo value="${this.fn}" lspace="0">${this.fn}</mo>`;
// TODO Implement other functions
return `<mi>${this.fn}</mi><mfenced>${args.join(COMMA)}</mfenced>`;
}
}

@@ -8,10 +8,12 @@ // =============================================================================

import { last } from '@mathigon/core'
import { SPECIAL_OPERATORS, SPECIAL_IDENTIFIERS, IDENTIFIER_SYMBOLS, OPERATOR_SYMBOLS } from './symbols'
import { ExprError, ExprNumber, ExprIdentifier, ExprOperator, ExprSpace, ExprString, ExprTerm } from "./expression";
import { last, words } from '@mathigon/core'
import { SPECIAL_OPERATORS, SPECIAL_IDENTIFIERS, IDENTIFIER_SYMBOLS, OPERATOR_SYMBOLS, BRACKETS } from './symbols'
import { ExprNumber, ExprIdentifier, ExprOperator, ExprSpace, ExprString, ExprTerm } from "./expression";
import { ExprFunction } from "./functions";
import { ExprError } from "./errors";
const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'};
// -----------------------------------------------------------------------------
// Tokenizer

@@ -22,3 +24,3 @@ function createToken(buffer, type) {

if (type === 'NUM') return new ExprNumber(+buffer);
if (type === 'SPACE' && buffer.length > 1) return [new ExprSpace()];
if (type === 'SPACE' && buffer.length > 1) return new ExprSpace();
if (type === 'STRING') return new ExprString(buffer);

@@ -67,3 +69,3 @@

OPERATOR_SYMBOLS.includes(s) ? 'OP' : s.match(/\s/) ? 'SPACE' : '';
if (!sType) throw new ExprError('Syntax Error', `Unknown symbol "${s}".`);
if (!sType) throw ExprError.invalidCharacter(s);

@@ -89,3 +91,5 @@ if (!type || (type === 'NUM' && sType !== 'NUM') ||

// -----------------------------------------------------------------------------
// Utility Functions

@@ -99,3 +103,3 @@ function makeTerm(items) {

for (let i of items) {
if (check[i]) {
if (check(i)) {
result.push([]);

@@ -109,3 +113,41 @@ } else {

function isOperator(expr, fns) {
return expr instanceof ExprOperator && words(fns).includes(expr.o);
}
function removeBrackets(expr) {
return (expr instanceof ExprFunction && expr.fn === '(') ? expr.args[0] : expr;
}
function findBinaryFunction(tokens, fn, toFn) {
if (isOperator(tokens[0], fn) || isOperator(tokens[tokens.length - 1], fn))
throw ExprError.startingOperator(fn);
for (let i = 1; i < tokens.length - 1; ++i) {
if (!isOperator(tokens[i], fn)) continue;
const a = tokens[i - 1];
const b = tokens[i + 1];
if (a instanceof ExprOperator) throw ExprError.consecutiveOperators(a.o, tokens[i].o);
if (b instanceof ExprOperator) throw ExprError.consecutiveOperators(tokens[i].o, b.o);
const args = [removeBrackets(a), removeBrackets(b)];
tokens.splice(i - 1, 3, new ExprFunction(toFn || tokens[i].o, args));
i -= 2;
}
}
// -----------------------------------------------------------------------------
// Match Brackets
function prepareTerm(tokens) {
// TODO Combine sup and sub calls into a single supsub function.
findBinaryFunction(tokens, '^', 'sup');
findBinaryFunction(tokens, '/');
return makeTerm(tokens);
}
export function matchBrackets(tokens) {
findBinaryFunction(tokens, '_', 'sub');
const stack = [[]];

@@ -116,6 +158,6 @@

if (')]}'.includes(t.o) || (t.o === '|' && lastOpen === '|')) {
if (isOperator(t, ') ] }') || (isOperator(t, '|') && lastOpen === '|')) {
if (t.o !== BRACKETS[lastOpen])
throw new ExprError('SyntaxError', `Unmatched bracket “${t.o}”.`);
if (!isOperator(t, BRACKETS[lastOpen]))
throw ExprError.conflictingBrackets(t.o);

@@ -126,10 +168,10 @@ const closed = stack.pop();

// Check if this is a normal bracket, or a function call.
const isFn = (t.o === ')' && last(term) instanceof ExprIdentifier);
const fnName = isFn ? term.pop().i : t.o === '|' ? 'abs' : closed[0];
const isFn = (isOperator(t, ')') && last(term) instanceof ExprIdentifier);
const fnName = isFn ? term.pop().i : isOperator(t, '|') ? 'abs' : closed[0].o;
// Support multiple arguments for function calls.
const args = splitArray(closed.slice(1), a => a.o === ',');
term.push(new ExprFunction(fnName, args.map(makeTerm)));
const args = splitArray(closed.slice(1), a => isOperator(a, ','));
term.push(new ExprFunction(fnName, args.map(prepareTerm)));
} else if('([{|'.includes(t.o)) {
} else if (isOperator(t, '( [ { |')) {
stack.push([t]);

@@ -142,51 +184,25 @@

if (stack.length > 1)
throw new ExprError('SyntaxError', `Unclosed bracket “${last(stack)[0]}”.`);
return makeTerm(stack[0]);
if (stack.length > 1) throw ExprError.unclosedBracket(last(stack)[0].o);
return prepareTerm(stack[0]);
}
// -----------------------------------------------------------------------------
// Collapse term items
/* function findBinaryFunction(tokens, chars) {
for (let i=0; i<tokens.length; ++i) {
if (chars.includes(tokens[i])) {
let a = tokens[i-1];
let b = tokens[i+1];
if (b == null) throw new ExprError('SyntaxError', `An expression cannot end with a “${tokens[i]}”.`);
if ('+-* / ^% ! ' .includes(a)) throw new ExprError('SyntaxError', `A “${a}” cannot be followed by a “${tokens[i]}”.`);
if ('+-* /^%!'.includes(b)) throw new ExprError('SyntaxError', `A “${tokens[i]}” cannot be followed by a “${b}”.`);
tokens.splice(i - 1, 3, [tokens[i], a, b]);
i -= 2;
}
}
}*/
export function collapseTerm(tokens) {
findBinaryFunction(tokens, '= < > ≤ ≥');
findBinaryFunction(tokens, '//', '/');
export function collapseTerm() {
// TODO Operators to functions, implicit multiplication, equals sign, ...
// TODO Support >2 arguments for + and *
// TODO Match multiplication and implicit multiplication
/*
findBinaryFunction(tokens, '^'); // Powers
findBinaryFunction(tokens, '* /'); // Multiplication and division.
// TODO Match starting - or ±
// Implicit multiplication (consecutive expressions)
for (let i=0; i<tokens.length-1; ++i) {
if (!'+-* /^%!'.includes(tokens[i]) && !'+-* /^%!'.includes(tokens[i+1])) {
tokens.splice(i, 2, ['*', tokens[i], tokens[i+1]]);
i -= 1;
}
}
findBinaryFunction(tokens, '-', '-');
findBinaryFunction(tokens, '±', '±');
// Leading (plus)minus.
if ('-±'.includes(tokens[0])) {
if (tokens.length <= 1) throw new ExprError('SyntaxError', `This expression is invalid.`);
tokens.splice(0, 2, [tokens[0], tokens[1]]);
}
// TODO Match addition
findBinaryFunction(tokens, '+-±'); // Addition and subtraction.
findBinaryFunction(tokens, '=<>≤≥'); // Equalities and inequalities.
if (tokens.length > 1) throw new ExprError('SyntaxError', `This expression is invalid.`);
return tokens[0]; */
if (tokens.length > 1) throw ExprError.invalidExpression();
return tokens[0];
}

@@ -8,6 +8,8 @@ // =============================================================================

export const BRACKETS = {'(': ')', '[': ']', '{': '}', '|': '|'};
export const SPECIAL_OPERATORS = {
'*': '·',
'**': '∗',
'//': '/',
'//': '//',
'+-': '±',

@@ -95,4 +97,4 @@ xx: '×',

const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~';
const SIMPLE_SYMBOLS = '|()[]{}÷,!<>=*/+-–~^_…';
const COMPLEX_SYMBOLS = Object.values(SPECIAL_OPERATORS);
export const OPERATOR_SYMBOLS = [...SIMPLE_SYMBOLS, ...COMPLEX_SYMBOLS];

@@ -15,3 +15,3 @@ // =============================================================================

tape('basic', function(test) {
tape('Basic', function(test) {
test.equal(mathML('42'), '<mn>42</mn>');

@@ -29,528 +29,55 @@ test.equal(mathML('3.141592654'), '<mn>3.141592654</mn>');

/* describe('Operators', function() {
it('Eulers number', function() {
test("e = sum_(n=0)^oo 1 / n!", '<math><mi>e</mi><mo>=</mo><munderover><mo>∑</mo><mrow><mi>n</mi><mo>=</mo><mn>0</mn></mrow><mi mathvariant="normal">∞</mi></munderover><mfrac><mn>1</mn><mrow><mi>n</mi><mo>!</mo></mrow></mfrac></math>');
});
it('Should remove space around derivatives', function() {
test("f'(x)", '<math><mi>f</mi><mo lspace="0" rspace="0">′</mo><mfenced open="(" close=")"><mi>x</mi></mfenced></math>');
test("f''(x)", '<math><mi>f</mi><mo lspace="0" rspace="0">″</mo><mfenced open="(" close=")"><mi>x</mi></mfenced></math>');
test("f'''(x)", '<math><mi>f</mi><mo lspace="0" rspace="0">‴</mo><mfenced open="(" close=")"><mi>x</mi></mfenced></math>');
test("f''''(x)", '<math><mi>f</mi><mo lspace="0" rspace="0">⁗</mo><mfenced open="(" close=")"><mi>x</mi></mfenced></math>');
});
it('Bayes theorem', function() {
test('p(a | b) = (p(b | a)p(a)) / p(b)', '<math><mi>p</mi><mfenced open="(" close=")"><mrow><mi>a</mi><mo stretchy="true" lspace="veryverythickmathspace" rspace="veryverythickmathspace">|</mo><mi>b</mi></mrow></mfenced><mo>=</mo><mfrac><mrow><mi>p</mi><mfenced open="(" close=")"><mrow><mi>b</mi><mo stretchy="true" lspace="veryverythickmathspace" rspace="veryverythickmathspace">|</mo><mi>a</mi></mrow></mfenced><mi>p</mi><mfenced open="(" close=")"><mi>a</mi></mfenced></mrow><mrow><mi>p</mi><mfenced open="(" close=")"><mi>b</mi></mfenced></mrow></mfrac></math>');
});
it('Gradient', function() {
test("grad f(x,y) = ((del f)/(del x) (x, y), (del f)/(del y) (x,y))",
'<math><mo rspace="0">∇</mo><mi>f</mi><mfenced open="(" close=")"><mi>x</mi><mi>y</mi></mfenced><mo>=</mo><mfenced open="(" close=")"><mrow><mfrac><mrow><mo rspace="0">∂</mo><mi>f</mi></mrow><mrow><mo rspace="0">∂</mo><mi>x</mi></mrow></mfrac><mfenced open="(" close=")"><mi>x</mi><mi>y</mi></mfenced></mrow><mrow><mfrac><mrow><mo rspace="0">∂</mo><mi>f</mi></mrow><mrow><mo rspace="0">∂</mo><mi>y</mi></mrow></mfrac><mfenced open="(" close=")"><mi>x</mi><mi>y</mi></mfenced></mrow></mfenced></math>');
});
it('Taylor polynomial', function() {
test("P_k(x) = f(a) + f'(a)(x-a) + (f''(a))/2! (x-a)^2 + cdots + (f^((k))(a))/k! (x-a)^k",
'<math><msub><mi>P</mi><mi>k</mi></msub><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>=</mo><mi>f</mi><mfenced open="(" close=")"><mi>a</mi></mfenced><mo>+</mo><mi>f</mi><mo lspace="0" rspace="0">′</mo><mfenced open="(" close=")"><mi>a</mi></mfenced><mfenced open="(" close=")"><mrow><mi>x</mi><mo>-</mo><mi>a</mi></mrow></mfenced><mo>+</mo><mfrac><mrow><mi>f</mi><mo lspace="0" rspace="0">″</mo><mfenced open="(" close=")"><mi>a</mi></mfenced></mrow><mrow><mn>2</mn><mo>!</mo></mrow></mfrac><msup><mfenced open="(" close=")"><mrow><mi>x</mi><mo>-</mo><mi>a</mi></mrow></mfenced><mn>2</mn></msup><mo>+</mo><mo>⋯</mo><mo>+</mo><mfrac><mrow><msup><mi>f</mi><mfenced open="(" close=")"><mi>k</mi></mfenced></msup><mfenced open="(" close=")"><mi>a</mi></mfenced></mrow><mrow><mi>k</mi><mo>!</mo></mrow></mfrac><msup><mfenced open="(" close=")"><mrow><mi>x</mi><mo>-</mo><mi>a</mi></mrow></mfenced><mi>k</mi></msup></math>');
});
it('Strokes theorem', function() {
test("oint_(del S) bf`F` * dbf`s` = dint_S grad xx bf`F` * dbf`s`",
'<math><msub><mo>∮</mo><mrow><mo rspace="0">∂</mo><mi>S</mi></mrow></msub><mi mathvariant="bold">F</mi><mo>·</mo><mi>d</mi><mi mathvariant="bold">s</mi><mo>=</mo><msub><mo>∬</mo><mi>S</mi></msub><mo rspace="0">∇</mo><mo>×</mo><mi mathvariant="bold">F</mi><mo>·</mo><mi>d</mi><mi mathvariant="bold">s</mi></math>');
});
tape('Whitespace', function(test) {
test.equal(mathML('a b'), '<mi>a</mi><mspace/><mi>b</mi>');
test.equal(mathML('a b'), '<mi>a</mi><mi>b</mi>');
test.end();
});
describe('Whitespace', function() {
it('Should keep whitespace if more then one space', function() {
test("a b", '<math><mi>a</mi><mspace width="1ex" /><mi>b</mi></math>');
test("a b", "<math><mi>a</mi><mi>b</mi></math>");
});
it('Should have width following the equation `n-1`ex', function() {
for (var n=2; n < 20; n++) {
test("a" + new Array(n+1).join(' ') + "b",
'<math><mi>a</mi><mspace width="' + (n-1) + 'ex" /><mi>b</mi></math>');
}
});
it('Should group adjacent symbols on either side of whitespace in an <mrow>', function() {
test('ab cd', "<math><mrow><mi>a</mi><mi>b</mi></mrow><mi>c</mi><mi>d</mi></math>");
});
it("But not straight after a function", function() {
test("sin (a + b)", '<math><mi>sin</mi><mfenced open="(" close=")"><mrow><mi>a</mi><mo>+</mo><mi>b</mi></mrow></mfenced></math>');
});
tape('Functions', function(test) {
test.equal(mathML('sin (a + b)'), '<mi>sin</mi><mfenced><mi>a</mi><mo value="+">+</mo><mi>b</mi></mfenced>');
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('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('log_b(x) = log_k(x)/log_k(b)'), 'xxx'); // TODO Support functions with subscripts
test.end();
});
describe("Identifier", function() {
it("Greeks (uppercase in normal variant, lowercase in italic)", function() {
test("Gamma Delta Theta Lambda Xi Pi Sigma Phi Psi Omega",
'<math><mi mathvariant="normal">Γ</mi><mi mathvariant="normal">Δ</mi><mi mathvariant="normal">Θ</mi><mi mathvariant="normal">Λ</mi><mi mathvariant="normal">Ξ</mi><mi mathvariant="normal">Π</mi><mi mathvariant="normal">Σ</mi><mi mathvariant="normal">Φ</mi><mi mathvariant="normal">Ψ</mi><mi mathvariant="normal">Ω</mi></math>');
test("alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi pi rho sigma tau upsilon phi chi psi omega",
"<math><mi>α</mi><mi>β</mi><mi>γ</mi><mi>δ</mi><mi>ɛ</mi><mi>ζ</mi><mi>η</mi><mi>θ</mi><mi>ι</mi><mi>κ</mi><mi>λ</mi><mi>μ</mi><mi>ν</mi><mi>ξ</mi><mi>π</mi><mi>ρ</mi><mi>σ</mi><mi>τ</mi><mi>υ</mi><mi>φ</mi><mi>χ</mi><mi>ψ</mi><mi>ω</mi></math>");
});
it('Special', function() {
test("oo O/", '<math><mi mathvariant="normal">∞</mi><mi mathvariant="normal">∅</mi></math>');
});
it('Blackboard', function() {
test("NN ZZ QQ RR CC", '<math><mi mathvariant="normal">ℕ</mi><mi mathvariant="normal">ℤ</mi><mi mathvariant="normal">ℚ</mi><mi mathvariant="normal">ℝ</mi><mi mathvariant="normal">ℂ</mi></math>');
});
it('Should force identifiers with (`)', function() {
test("`int`", "<math><mi>int</mi></math>");
});
});
describe('Standard functions', function() {
it("Tangent = Sinus over Cosinus", function() {
test("tan = sin/cos",
"<math><mrow><mi>tan</mi><mo>=</mo></mrow><mfrac><mi>sin</mi><mi>cos</mi></mfrac></math>");
});
it("The hyperbolic functions", function() {
test("sinh x = (e^x - e^(-x))/2, cosh x = (e^x + e^(-x))/2, tanh x = (sinh x)/(cosh x)",
'<math><mrow><mi>sinh</mi><mi>x</mi></mrow><mo>=</mo><mfrac><mrow><msup><mi>e</mi><mi>x</mi></msup><mo>-</mo><msup><mi>e</mi><mrow><mo>-</mo><mi>x</mi></mrow></msup></mrow><mn>2</mn></mfrac><mo>,</mo><mrow><mi>cosh</mi><mi>x</mi></mrow><mo>=</mo><mfrac><mrow><msup><mi>e</mi><mi>x</mi></msup><mo>+</mo><msup><mi>e</mi><mrow><mo>-</mo><mi>x</mi></mrow></msup></mrow><mn>2</mn></mfrac><mo>,</mo><mrow><mi>tanh</mi><mi>x</mi></mrow><mo>=</mo><mfrac><mrow><mi>sinh</mi><mi>x</mi></mrow><mrow><mi>cosh</mi><mi>x</mi></mrow></mfrac></math>');
});
it("Logarithm change of base", function() {
test("log_b x = (log_k x)/(log_k b)", "<math><msub><mi>log</mi><mi>b</mi></msub><mi>x</mi><mo>=</mo><mfrac><mrow><msub><mi>log</mi><mi>k</mi></msub><mi>x</mi></mrow><mrow><msub><mi>log</mi><mi>k</mi></msub><mi>b</mi></mrow></mfrac></math>");
});
it("Logarithm powers", function() {
test("ln x^2 = 2 ln x", "<math><mrow><mi>ln</mi><msup><mi>x</mi><mn>2</mn></msup></mrow><mo>=</mo><mn>2</mn><mrow><mi>ln</mi><mi>x</mi></mrow></math>");
});
it("Logarithm division", function() {
test("ln(x/y) = ln x - ln y",
'<math><mi>ln</mi><mfenced open="(" close=")"><mfrac><mi>x</mi><mi>y</mi></mfrac></mfenced><mo>=</mo><mrow><mi>ln</mi><mi>x</mi></mrow><mo>-</mo><mrow><mi>ln</mi><mi>y</mi></mrow></math>');
});
it("2×2 determinants", function() {
test("det(A) = |(a, b), (c, d)| = ad - cd", '<math><mi>det</mi><mfenced open="(" close=")"><mi>A</mi></mfenced><mo>=</mo><mfenced open="|" close="|"><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd><mtd><mi>d</mi></mtd></mtr></mtable></mfenced><mo>=</mo><mrow><mi>a</mi><mi>d</mi></mrow><mo>-</mo><mi>c</mi><mi>d</mi></math>');
});
it("Fermats little theorem", function() {
test("a^(p-1) -= 1 (mod p)",
'<math><msup><mi>a</mi><mrow><mi>p</mi><mo>-</mo><mn>1</mn></mrow></msup><mo>≡</mo><mn>1</mn><mspace width="2ex" /><mfenced open="(" close=")"><mrow><mi>mod</mi><mi>p</mi></mrow></mfenced></math>');
});
});
describe('Fractions', function() {
it("Should display fractions", function() {
test("a/b", "<math><mfrac><mi>a</mi><mi>b</mi></mfrac></math>");
});
it('Should have bevelled fractions', function() {
test("a./b", '<math><mfrac bevelled="true"><mi>a</mi><mi>b</mi></mfrac></math>');
});
it('Should not parse `//` as fraction', function() {
test('a//b', '<math><mi>a</mi><mo>/</mo><mi>b</mi></math>');
});
it("Should not display brackets around numerator or denominator", function() {
test("(a+b)/(c+d)", "<math><mfrac><mrow><mi>a</mi><mo>+</mo><mi>b</mi></mrow><mrow><mi>c</mi><mo>+</mo><mi>d</mi></mrow></mfrac></math>");
});
it("Should not fail on trailing fractions", function() {
test("a/", "<math><mfrac><mi>a</mi><mrow></mrow></mfrac></math>");
test("b ./ ", '<math><mfrac bevelled="true"><mi>b</mi><mrow></mrow></mfrac></math>');
});
it('Should have whitespace delimited fractions', function() {
test("1+3 / 2+2", "<math><mfrac><mrow><mn>1</mn><mo>+</mo><mn>3</mn></mrow><mrow><mn>2</mn><mo>+</mo><mn>2</mn></mrow></mfrac></math>");
test("1 + 3/2 + 2", "<math><mn>1</mn><mo>+</mo><mfrac><mn>3</mn><mn>2</mn></mfrac><mo>+</mo><mn>2</mn></math>");
test("a./b / c./d", '<math><mfrac><mfrac bevelled="true"><mi>a</mi><mi>b</mi></mfrac><mfrac bevelled="true"><mi>c</mi><mi>d</mi></mfrac></mfrac></math>');
});
it("Golden ratio (continued fraction)", function() {
test("phi = 1 + 1/(1 + 1/(1 + 1/(1 + 1/(1 + ddots))))",
"<math><mi>φ</mi><mo>=</mo><mn>1</mn><mo>+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo>+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo>+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo>+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo>+</mo><mo>⋱</mo></mrow></mfrac></mrow></mfrac></mrow></mfrac></mrow></mfrac></math>");
});
it("Normal distribution", function() {
test('cc`N`(x | mu, sigma^2) = 1/(sqrt(2pi sigma^2)) e^(-((x-mu)^2) / 2sigma^2)',
'<math><mi mathvariant="script">N</mi><mfenced open="(" close=")"><mrow><mi>x</mi><mo stretchy="true" lspace="veryverythickmathspace" rspace="veryverythickmathspace">|</mo><mi>μ</mi></mrow><msup><mi>σ</mi><mn>2</mn></msup></mfenced><mo>=</mo><mfrac><mn>1</mn><msqrt><mrow><mn>2</mn><mi>π</mi><msup><mi>σ</mi><mn>2</mn></msup></mrow></msqrt></mfrac><msup><mi>e</mi><mrow><mo>-</mo><mfrac><msup><mfenced open="(" close=")"><mrow><mi>x</mi><mo>-</mo><mi>μ</mi></mrow></mfenced><mn>2</mn></msup><mrow><mn>2</mn><msup><mi>σ</mi><mn>2</mn></msup></mrow></mfrac></mrow></msup></math>');
});
});
describe('Roots', function() {
it("Should display square roots", function() {
test("sqrt x", "<math><msqrt><mi>x</mi></msqrt></math>");
});
it("Should display n-roots", function() {
test("root n x", "<math><mroot><mi>x</mi><mi>n</mi></mroot></math>");
});
it("Should not display brackets in roots", function() {
test("sqrt(2)", "<math><msqrt><mn>2</mn></msqrt></math>");
test("root(3)(2)", "<math><mroot><mn>2</mn><mn>3</mn></mroot></math>");
});
it("Should not fail in empty roots", function() {
test("sqrt", "<math><msqrt><mrow></mrow></msqrt></math>");
test("root ", "<math><mroot><mrow></mrow><mrow></mrow></mroot></math>");
test("root 3 ", "<math><mroot><mrow></mrow><mn>3</mn></mroot></math>");
});
it("sqrt(2) ≈ 1.414", function() {
test("sqrt 2 ~~ 1.414213562", "<math><msqrt><mn>2</mn></msqrt><mo>≈</mo><mn>1.414213562</mn></math>");
});
it("Quadradic formula", function() {
test("x = (-b +- sqrt(b^2 - 4ac)) / 2a",
"<math><mi>x</mi><mo>=</mo><mfrac><mrow><mo>-</mo><mi>b</mi><mo>±</mo><msqrt><mrow><msup><mi>b</mi><mn>2</mn></msup><mo>-</mo><mn>4</mn><mi>a</mi><mi>c</mi></mrow></msqrt></mrow><mrow><mn>2</mn><mi>a</mi></mrow></mfrac></math>");
});
it("Golden ratio (algebraic form)", function() {
test("phi = (1 + sqrt 5)/2", "<math><mi>φ</mi><mo>=</mo><mfrac><mrow><mn>1</mn><mo>+</mo><msqrt><mn>5</mn></msqrt></mrow><mn>2</mn></mfrac></math>");
});
it("Plastic number", function() {
test("rho = (root3(108 + 12 sqrt 69) + root3(108 - 12 sqrt 69)) / 6",
"<math><mi>ρ</mi><mo>=</mo><mfrac><mrow><mroot><mrow><mn>108</mn><mo>+</mo><mn>12</mn><msqrt><mn>69</mn></msqrt></mrow><mn>3</mn></mroot><mo>+</mo><mroot><mrow><mn>108</mn><mo>-</mo><mn>12</mn><msqrt><mn>69</mn></msqrt></mrow><mn>3</mn></mroot></mrow><mn>6</mn></mfrac></math>");
});
it("Continued square root", function() {
test("sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + sqrt(1 + cdots)))))))",
"<math><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><msqrt><mrow><mn>1</mn><mo>+</mo><mo>⋯</mo></mrow></msqrt></mrow></msqrt></mrow></msqrt></mrow></msqrt></mrow></msqrt></mrow></msqrt></mrow></msqrt></math>");
});
});
describe('Groupings', function() {
it('Should group brackets together', function() {
test('(a+b)', '<math><mfenced open="(" close=")"><mrow><mi>a</mi><mo>+</mo><mi>b</mi></mrow></mfenced></math>');
});
it('Should handle comma seperated lists', function() {
test('a,b,c', '<math><mi>a</mi><mo>,</mo><mi>b</mi><mo>,</mo><mi>c</mi></math>');
});
it('Should add parentesis around parentesized comma seperated lists', function() {
test('(a,b,c)', '<math><mfenced open="(" close=")"><mi>a</mi><mi>b</mi><mi>c</mi></mfenced></math>');
});
it("Should not fail on unclosed fences", function() {
test("(a", '<math><mfenced open="(" close=""><mi>a</mi></mfenced></math>');
test("((a)", '<math><mfenced open="(" close=""><mfenced open="(" close=")"><mi>a</mi></mfenced></mfenced></math>');
test("[(", '<math><mfenced open="[" close=""><mfenced open="(" close=""></mfenced></mfenced></math>');
});
it('Simplify polynomials', function() {
test("(x+y)(x-y) = x^2-y^2", '<math><mfenced open="(" close=")"><mrow><mi>x</mi><mo>+</mo><mi>y</mi></mrow></mfenced><mfenced open="(" close=")"><mrow><mi>x</mi><mo>-</mo><mi>y</mi></mrow></mfenced><mo>=</mo><msup><mi>x</mi><mn>2</mn></msup><mo>-</mo><msup><mi>y</mi><mn>2</mn></msup></math>');
});
it('Exponential decay', function() {
test("e^(-x)", "<math><msup><mi>e</mi><mrow><mo>-</mo><mi>x</mi></mrow></msup></math>");
});
it('Eulers identity', function() {
test("e^(i tau) = 1", "<math><msup><mi>e</mi><mrow><mi>i</mi><mi>τ</mi></mrow></msup><mo>=</mo><mn>1</mn></math>");
});
it("The natural numbers", function() {
test('NN = {1, 2, 3, ...}', '<math><mi mathvariant="normal">ℕ</mi><mo>=</mo><mfenced open="{" close="}"><mn>1</mn><mn>2</mn><mn>3</mn><mo>…</mo></mfenced></math>');
});
it("Average over time", function() {
test('(: V(t)^2 :) = lim_(T->oo) 1/T int_(-T./2)^(T./2) V(t)^2 dt',
'<math><mfenced open="⟨" close="⟩"><mrow><mi>V</mi><msup><mfenced open="(" close=")"><mi>t</mi></mfenced><mn>2</mn></msup></mrow></mfenced><mo>=</mo><munder><mi>lim</mi><mrow><mi>T</mi><mo>→</mo><mi mathvariant="normal">∞</mi></mrow></munder><mfrac><mn>1</mn><mi>T</mi></mfrac><msubsup><mo>∫</mo><mrow><mo>-</mo><mfrac bevelled="true"><mi>T</mi><mn>2</mn></mfrac></mrow><mfrac bevelled="true"><mi>T</mi><mn>2</mn></mfrac></msubsup><mi>V</mi><msup><mfenced open="(" close=")"><mi>t</mi></mfenced><mn>2</mn></msup><mi>d</mi><mi>t</mi></math>');
});
it('Complex groupings', function() {
test('abs(x)', '<math><mfenced open="|" close="|"><mi>x</mi></mfenced></math>');
test('floor(x)', '<math><mfenced open="⌊" close="⌋"><mi>x</mi></mfenced></math>');
test('ceil(x)', '<math><mfenced open="⌈" close="⌉"><mi>x</mi></mfenced></math>');
test('norm(x)', '<math><mfenced open="∥" close="∥"><mi>x</mi></mfenced></math>');
test('abs x', '<math><mfenced open="|" close="|"><mi>x</mi></mfenced></math>');
test('floor x', '<math><mfenced open="⌊" close="⌋"><mi>x</mi></mfenced></math>');
test('ceil x', '<math><mfenced open="⌈" close="⌉"><mi>x</mi></mfenced></math>');
test('norm x', '<math><mfenced open="∥" close="∥"><mi>x</mi></mfenced></math>');
});
});
describe('Super and subscripts', function() {
it('Should display subscripts', function() {
test("a_i", "<math><msub><mi>a</mi><mi>i</mi></msub></math>");
});
it('Should display superscripts', function() {
test("a^2", "<math><msup><mi>a</mi><mn>2</mn></msup></math>");
});
it('Should display sub-superscripts', function() {
test("a_i^2", "<math><msubsup><mi>a</mi><mi>i</mi><mn>2</mn></msubsup></math>");
});
it('Should render sub-superscripts in either direction', function() {
test("a^2_i", "<math><msubsup><mi>a</mi><mi>i</mi><mn>2</mn></msubsup></math>");
});
it("Should not throw on trailing sub or superscripts", function() {
test("a^", "<math><msup><mi>a</mi><mrow></mrow></msup></math>");
test("b_ ", "<math><msub><mi>b</mi><mrow></mrow></msub></math>");
});
it("Should not throw on trailing under or overscripts", function() {
test("a.^", "<math><mover><mi>a</mi><mrow></mrow></mover></math>");
test("b._ ", "<math><munder><mi>b</mi><mrow></mrow></munder></math>");
});
it("Should not throw on trailing sub-supscript", function() {
test("a_i^", "<math><msubsup><mi>a</mi><mi>i</mi><mrow></mrow></msubsup></math>");
test("a^2_", "<math><msubsup><mi>a</mi><mrow></mrow><mn>2</mn></msubsup></math>");
});
it("Should not throw on trailing under-overscript", function() {
expect(ascii2mathml).withArgs("a._i .^").not.to.throwException();
expect(ascii2mathml).withArgs("a._i ^").not.to.throwException();
expect(ascii2mathml).withArgs("a.^2._ ").not.to.throwException();
expect(ascii2mathml).withArgs("a.^ 2_ ").not.to.throwException();
});
it('Should not treat `^^` as superscript', function() {
test('a^^2', '<math><mi>a</mi><mo>∧</mo><mn>2</mn></math>');
});
it('Should not treat `__` or `_|` as subscript', function() {
test('a_|_i', '<math><mi>a</mi><mo>⊥</mo><mi>i</mi></math>');
test('|__a__|i', '<math><mo>⌊</mo><mi>a</mi><mo>⌋</mo><mi>i</mi></math>');
});
it('Pythagorean theorem', function() {
test("a^2 + b^2 = c^2", "<math><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup><mo>=</mo><msup><mi>c</mi><mn>2</mn></msup></math>");
});
it('Matrix transpose', function() {
test("(X^T)_(ij) = X_(ji)",
'<math><msub><mfenced open="(" close=")"><msup><mi>X</mi><mi>T</mi></msup></mfenced><mrow><mi>i</mi><mi>j</mi></mrow></msub><mo>=</mo><msub><mi>X</mi><mrow><mi>j</mi><mi>i</mi></mrow></msub></math>');
});
it('The natural logarithm', function() {
test("ln x = int_1^x 1/t dt", "<math><mrow><mi>ln</mi><mi>x</mi></mrow><mo>=</mo><msubsup><mo>∫</mo><mn>1</mn><mi>x</mi></msubsup><mfrac><mn>1</mn><mi>t</mi></mfrac><mi>d</mi><mi>t</mi></math>");
});
it('Powers of powers of two', function() {
test("2^2^2^2", "<math><msup><mn>2</mn><msup><mn>2</mn><msup><mn>2</mn><mn>2</mn></msup></msup></msup></math>");
});
});
describe('Under and overscripts', function() {
it('Should display underscripts', function() {
test('X._a', "<math><munder><mi>X</mi><mi>a</mi></munder></math>");
});
it('Should display overscripts', function() {
test('X.^a', "<math><mover><mi>X</mi><mi>a</mi></mover></math>");
});
it('Should display overscripts', function() {
test('X.^a._b', "<math><munderover><mi>X</mi><mi>b</mi><mi>a</mi></munderover></math>");
});
it('Should go under limits', function() {
test('lim_(a -> b)', "<math><munder><mi>lim</mi><mrow><mi>a</mi><mo>→</mo><mi>b</mi></mrow></munder></math>");
test('lim_(a->b)', "<math><munder><mi>lim</mi><mrow><mi>a</mi><mo>→</mo><mi>b</mi></mrow></munder></math>");
});
it('Should go over and under sums', function() {
test('sum_(i=0)^n', "<math><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>0</mn></mrow><mi>n</mi></munderover></math>");
test('sum^n_(i=0)', "<math><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>0</mn></mrow><mi>n</mi></munderover></math>");
});
it("Should go over and under products", function() {
test('prod_(i=1)^n', "<math><munderover><mo>∏</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover></math>");
test('prod^n_(i=1)', "<math><munderover><mo>∏</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover></math>");
});
it("Golden ratio (defenition)", function() {
test('phi =.^"def" a/b = (a+b)/a', "<math><mi>φ</mi><mover><mo>=</mo><mtext>def</mtext></mover><mfrac><mi>a</mi><mi>b</mi></mfrac><mo>=</mo><mfrac><mrow><mi>a</mi><mo>+</mo><mi>b</mi></mrow><mi>a</mi></mfrac></math>");
});
it("Matrix dimentions", function() {
test('X._(n xx m)', "<math><munder><mi>X</mi><mrow><mi>n</mi><mo>×</mo><mi>m</mi></mrow></munder></math>");
});
it("k times x", function() {
test('{: x + ... + x :}.^⏞.^(k "times")', '<math><mover><mfenced open="" close=""><mrow><mi>x</mi><mo>+</mo><mo>…</mo><mo>+</mo><mi>x</mi></mrow></mfenced><mover><mo>⏞</mo><mrow><mi>k</mi><mspace width="1ex" /><mtext>times</mtext></mrow></mover></mover></math>');
});
});
describe('Matrices', function() {
it('Should display column vectors', function() {
test("(1;2;3)", '<math><mfenced open="(" close=")"><mtable><mtr><mtd><mn>1</mn></mtd></mtr><mtr><mtd><mn>2</mn></mtd></mtr><mtr><mtd><mn>3</mn></mtd></mtr></mtable></mfenced></math>');
});
it('Should display matrices', function() {
test('[[a, b], [c, d]]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd><mtd><mi>d</mi></mtd></mtr></mtable></mfenced></math>');
});
it('Should display matrices with semicolon as a column separator', function() {
expect(ascii2mathml('[[a; b]; [c; d]]', {decimalMark: ','}))
.to.be(ascii2mathml('[[a, b], [c, d]]'));
});
it('Should accept newlines instead of commas', function() {
expect(ascii2mathml('[[a, b]\n [c, d]]'))
.to.be(ascii2mathml('[[a, b], [c, d]]'));
expect(ascii2mathml('[[a, b],\n [c, d]]'))
.to.be(ascii2mathml('[[a, b], [c, d]]'));
});
it('Should accept newlines instead of semicolons (w. `;` as colSep)', function() {
expect(ascii2mathml('[[a; b]\n [c; d]]', {colSep: ';'}))
.to.be(ascii2mathml('[[a, b], [c, d]]'));
expect(ascii2mathml('[[a; b];\n [c; d]]', {colSep: ';'}))
.to.be(ascii2mathml('[[a, b], [c, d]]'));
});
it('Should display matrices using comma, semicolon syntax', function() {
test('[1, 2, 3; 4, 5, 6]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd><mn>3</mn></mtd></mtr><mtr><mtd><mn>4</mn></mtd><mtd><mn>5</mn></mtd><mtd><mn>6</mn></mtd></mtr></mtable></mfenced></math>');
});
it('Should allow trailing row brakes', function() {
test('[1, 2, 3;]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd><mn>3</mn></mtd></mtr></mtable></mfenced></math>');
test('[(1, 2, 3),]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd><mn>3</mn></mtd></mtr></mtable></mfenced></math>');
});
it('Should allow newlines as row separators', function() {
expect(ascii2mathml('[1, 2, 3\n 4, 5, 6]'))
.to.be(ascii2mathml('[1, 2, 3; 4, 5, 6]'));
expect(ascii2mathml('[a\n b\n c]'))
.to.be(ascii2mathml('[a; b; c]'));
expect(ascii2mathml('(4\n 6)'))
.to.be(ascii2mathml('(4; 6)'));
});
it('Should allow comma/semicolon to take precedence over bracket delimited matrices', function() {
test('[(a), (b); (c), (d)]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mfenced open="(" close=")"><mi>a</mi></mfenced></mtd><mtd><mfenced open="(" close=")"><mi>b</mi></mfenced></mtd></mtr><mtr><mtd><mfenced open="(" close=")"><mi>c</mi></mfenced></mtd><mtd><mfenced open="(" close=")"><mi>d</mi></mfenced></mtd></mtr></mtable></mfenced></math>');
});
it('Should allow newlines to take precedence over bracket delimited matrices', function() {
test('[(a), (b)\n (c), (d)]', '<math><mfenced open="[" close="]"><mtable><mtr><mtd><mfenced open="(" close=")"><mi>a</mi></mfenced></mtd><mtd><mfenced open="(" close=")"><mi>b</mi></mfenced></mtd></mtr><mtr><mtd><mfenced open="(" close=")"><mi>c</mi></mfenced></mtd><mtd><mfenced open="(" close=")"><mi>d</mi></mfenced></mtd></mtr></mtable></mfenced></math>');
});
it('Should display vertical bar delimited matrices', function() {
test('|(a,b,c), (d,e,f), (h,i,j)|', '<math><mfenced open="|" close="|"><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd><mtd><mi>c</mi></mtd></mtr><mtr><mtd><mi>d</mi></mtd><mtd><mi>e</mi></mtd><mtd><mi>f</mi></mtd></mtr><mtr><mtd><mi>h</mi></mtd><mtd><mi>i</mi></mtd><mtd><mi>j</mi></mtd></mtr></mtable></mfenced></math>');
});
it('Should display double vertical bar delimited matrices', function() {
test('|| a ; b ; c ||', '<math><mfenced open="‖" close="‖"><mtable><mtr><mtd><mi>a</mi></mtd></mtr><mtr><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>c</mi></mtd></mtr></mtable></mfenced></math>');
});
it('The general n×m matrix', function() {
test('A = [[a_(1 1), a_(1 2), cdots, a_(1 n)], [a_(2 1), a_(2 2), cdots, a_(2 n)], [vdots, vdots, ddots, vdots], [a_(m 1), a_(m 2), cdots, a_(m n)]]',
'<math><mi>A</mi><mo>=</mo><mfenced open="[" close="]"><mtable><mtr><mtd><msub><mi>a</mi><mrow><mn>1</mn><mn>1</mn></mrow></msub></mtd><mtd><msub><mi>a</mi><mrow><mn>1</mn><mn>2</mn></mrow></msub></mtd><mtd><mo>⋯</mo></mtd><mtd><msub><mi>a</mi><mrow><mn>1</mn><mi>n</mi></mrow></msub></mtd></mtr><mtr><mtd><msub><mi>a</mi><mrow><mn>2</mn><mn>1</mn></mrow></msub></mtd><mtd><msub><mi>a</mi><mrow><mn>2</mn><mn>2</mn></mrow></msub></mtd><mtd><mo>⋯</mo></mtd><mtd><msub><mi>a</mi><mrow><mn>2</mn><mi>n</mi></mrow></msub></mtd></mtr><mtr><mtd><mo>⋮</mo></mtd><mtd><mo>⋮</mo></mtd><mtd><mo>⋱</mo></mtd><mtd><mo>⋮</mo></mtd></mtr><mtr><mtd><msub><mi>a</mi><mrow><mi>m</mi><mn>1</mn></mrow></msub></mtd><mtd><msub><mi>a</mi><mrow><mi>m</mi><mn>2</mn></mrow></msub></mtd><mtd><mo>⋯</mo></mtd><mtd><msub><mi>a</mi><mrow><mi>m</mi><mi>n</mi></mrow></msub></mtd></mtr></mtable></mfenced></math>');
});
it('Nested matrices', function() {
test("[[((a, b), (d, e)), -1], [1, ((f, g), (h, i))]]",
'<math><mfenced open="[" close="]"><mtable><mtr><mtd><mfenced open="(" close=")"><mtable><mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr><mtr><mtd><mi>d</mi></mtd><mtd><mi>e</mi></mtd></mtr></mtable></mfenced></mtd><mtd><mo>-</mo><mn>1</mn></mtd></mtr><mtr><mtd><mn>1</mn></mtd><mtd><mfenced open="(" close=")"><mtable><mtr><mtd><mi>f</mi></mtd><mtd><mi>g</mi></mtd></mtr><mtr><mtd><mi>h</mi></mtd><mtd><mi>i</mi></mtd></mtr></mtable></mfenced></mtd></mtr></mtable></mfenced></math>');
});
it('The binomial coefficient', function() {
test("(n; k) = n! / (n-k)!k!",
'<math><mfenced open="(" close=")"><mfrac linethickness="0"><mi>n</mi><mi>k</mi></mfrac></mfenced><mo>=</mo><mfrac><mrow><mi>n</mi><mo>!</mo></mrow><mrow><mfenced open="(" close=")"><mrow><mi>n</mi><mo>-</mo><mi>k</mi></mrow></mfenced><mo>!</mo><mi>k</mi><mo>!</mo></mrow></mfrac></math>');
});
it("The absolute value", function() {
test('|x| = { (x\\,, if x >= 0), (-x\\,, if x < 0) :}',
'<math><mfenced open="|" close="|"><mi>x</mi></mfenced><mo>=</mo><mfenced open="{" close=""><mtable columnalign="center left"><mtr><mtd><mi>x</mi><mo>,</mo></mtd><mtd><mo>if</mo><mi>x</mi><mo>≥</mo><mn>0</mn></mtd></mtr><mtr><mtd><mo>-</mo><mi>x</mi><mo>,</mo></mtd><mtd><mo>if</mo><mi>x</mi><mo>&lt;</mo><mn>0</mn></mtd></mtr></mtable></mfenced></math>');
});
it('Factorial', function() {
test('n! = { [1, if n=0 or n=1], [n(n-1)!, if n > 1] :}',
'<math><mrow><mi>n</mi><mo>!</mo></mrow><mo>=</mo><mfenced open="{" close=""><mtable columnalign="center left"><mtr><mtd><mn>1</mn></mtd><mtd><mo>if</mo><mi>n</mi><mo>=</mo><mn>0</mn><mo>or</mo><mi>n</mi><mo>=</mo><mn>1</mn></mtd></mtr><mtr><mtd><mi>n</mi><mfenced open="(" close=")"><mrow><mi>n</mi><mo>-</mo><mn>1</mn></mrow></mfenced><mo>!</mo></mtd><mtd><mo>if</mo><mi>n</mi><mo>&gt;</mo><mn>1</mn></mtd></mtr></mtable></mfenced></math>');
});
});
describe('Accents', function() {
it('Should display accents', function() {
test('hat x', '<math><mover><mi>x</mi><mo accent="true">^</mo></mover></math>');
test('bar x', '<math><mover><mi>x</mi><mo accent="true">‾</mo></mover></math>');
test('ul x', '<math><munder><mi>x</mi><mo>_</mo></munder></math>');
test('vec x', '<math><mover><mi>x</mi><mo accent="true">→</mo></mover></math>');
test('dot x', '<math><mover><mi>x</mi><mo accent="true">⋅</mo></mover></math>');
test('ddot x', '<math><mover><mi>x</mi><mo accent="true">⋅⋅</mo></mover></math>');
test('tilde x', '<math><mover><mi>x</mi><mo accent="true">˜</mo></mover></math>');
test('cancel x', '<math><menclose notation="updiagonalstrike"><mi>x</mi></menclose></math>');
});
it('Should display dottless i and dottless j under overscript accents', function() {
test('bar i', '<math><mover><mi>ı</mi><mo accent="true">‾</mo></mover></math>');
test('vec j', '<math><mover><mi>ȷ</mi><mo accent="true">→</mo></mover></math>');
test('ul i', '<math><munder><mi>i</mi><mo>_</mo></munder></math>');
});
it('Should put accents over all the following parenthesis', function() {
test("3hat(xyz)", '<math><mn>3</mn><mover><mrow><mi>x</mi><mi>y</mi><mi>z</mi></mrow><mo accent="true">^</mo></mover></math>');
});
it('Physics vector notation', function() {
test('vec x = ahat i + bhat j + chat k',
'<math><mover><mi>x</mi><mo accent="true">→</mo></mover><mo>=</mo><mrow><mi>a</mi><mover><mi>ı</mi><mo accent="true">^</mo></mover></mrow><mo>+</mo><mrow><mi>b</mi><mover><mi>ȷ</mi><mo accent="true">^</mo></mover></mrow><mo>+</mo><mrow><mi>c</mi><mover><mi>k</mi><mo accent="true">^</mo></mover></mrow></math>');
});
/*tape('Implicit evaluation', function(test) {
test.equal(expr('1 + 2').toString(), '3');
test.equal(expr('1 - 2').toString(), '-1');
test.equal(expr('2*3').toString(), '6');
test.equal(expr('5/2').toString(), '2.5');
test.equal(expr('2^3').toString(), '8');
test.equal(expr('3+2/4+2*8').toString(), '19.5');
test.equal(expr('sqrt(9)').toString(), '3');
test.equal(expr('3+sin(pi)').toString(), '3');
test.equal(expr('5%').toString(), '0.05');
test.equal(expr('log(e)').toString(), '1');
tape('Fractions', function(test) {
test.equal(mathML('a/b'), '<mfrac><mi>a</mi><mi>b</mi></mfrac>');
test.equal(mathML('a//b'), '<mi>a</mi><mo value="/">/</mo><mi>b</mi>');
test.equal(mathML('(a+b)/(c+d)'), '<mfrac><mrow><mi>a</mi><mo value="+">+</mo><mi>b</mi></mrow><mrow><mi>c</mi><mo value="+">+</mo><mi>d</mi></mrow></mfrac>');
test.equal(mathML('phi = 1 + 1/(1 + 1/(1 + 1/(1 + 1/(1 + …))))'), '<mi>φ</mi><mo value="=">=</mo><mn>1</mn><mo value="+">+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo value="+">+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo value="+">+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo value="+">+</mo><mfrac><mn>1</mn><mrow><mn>1</mn><mo value="+">+</mo><mo value="…">…</mo></mrow></mfrac></mrow></mfrac></mrow></mfrac></mrow></mfrac>');
test.end();
});
tape('Function and variable extraction', function(test) {
test.deepEqual(expr('a + b(c - d(5))').variables, ['a', 'c']);
test.deepEqual(expr('a + b(c - d(5))').functions, ['+', 'b', '-', 'd']);
tape('Super and Subscripts', function(test) {
test.equal(mathML('a_i'), '<msub><mi>a</mi><mi>i</mi></msub>');
test.equal(mathML('a^2'), '<msup><mi>a</mi><mn>2</mn></msup>');
test.equal(mathML('a^2 + b^2 = c^2'), '<msup><mi>a</mi><mn>2</mn></msup><mo value="+">+</mo><msup><mi>b</mi><mn>2</mn></msup><mo value="=">=</mo><msup><mi>c</mi><mn>2</mn></msup>');
// test.equal(mathML('2^2^2^2'), '<msup><mrow><msup><mrow><msup><mn>2</mn><mn>2</mn></msup></mrow><mn>2</mn></msup></mrow><mn>2</mn></msup>'); // TODO Right-to-left
// test.equal(mathML('a_i^2'), '<msubsup><mi>a</mi><mi>i</mi><mn>2</mn></msubsup>'); // TODO Support Super and Subscripts
// test.equal(mathML('a^2_i'), '<msubsup><mi>a</mi><mi>i</mi><mn>2</mn></msubsup>'); // TODO Support Super and Subscripts
test.end();
});
tape('Direct evaluation', function(test) {
test.equal(expr('a + b + sqrt(1)').evaluate({a: 1, b: 2}), 4);
test.equal(expr('a + b(a)').evaluate({a: 10, b: (x => x/2)}), 15);
tape('Roots', function(test) {
test.equal(mathML('sqrt(x)'), '<msqrt><mi>x</mi></msqrt>');
test.equal(mathML('root(x, 3)'), '<mroot><mi>x</mi><mn>3</mn></mroot>');
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('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>');
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.end();
});
tape('Basic simplification', function(test) {
test.equal(expr('x+x').toString(), '2*x');
test.equal(expr('2x+x').toString(), '3*x');
test.equal(expr('2(x+1)+(x+1)').toString(), '3*(x + 1)');
test.equal(expr('y*x^2+2*x^2').toString(), '(y+2)*x^2');
test.equal(expr('x+1+x').toString(), '(2*x) + 1');
test.equal(expr('x^2+x+3+x^2').toString(), '(2*(x^2)) + x + 3');
test.equal(expr('x+1+2x').toString(), '(3*x) + 1');
test.equal(expr('x-1+x').toString(), '(2*x) - 1');
test.equal(expr('x-1-2x+2').toString(), '1 - x');
test.equal(expr('x/2*x').toString(), '(x^2)/2');
test.equal(expr('x*2*x').toString(), '2*(x^2)');
test.equal(expr('x*y*(-x)/(x^2)').toString(), '-y');
test.equal(expr('foo(x)').toString(), 'foo(x)');
tape('Groupings', function(test) {
test.equal(mathML('(a+b)'), '<mfenced open="(" close=")"><mi>a</mi><mo value="+">+</mo><mi>b</mi></mfenced>');
test.equal(mathML('a,b,c'), '<mi>a</mi><mo value=",">,</mo><mi>b</mi><mo value=",">,</mo><mi>c</mi>');
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('e^(i tau) = 1'), '<msup><mi>e</mi><mrow><mi>i</mi><mi>τ</mi></mrow></msup><mo value="=">=</mo><mn>1</mn>');
test.end();
});
tape('Advanced simplification', function(test) {
test.equal(expr('1/x + 1/y').toString(), '(x + y)/(x*y)');
test.equal(expr('4 + 4*x - 2*(2 + 2*x))/(2 + 2*x)').toString(), '0');
test.equal(expr('-4*x*y^2 - 2*y^3 - 2*x^2*y)/(x + y)^2').toString(), '-2*y');
test.equal(expr('-x - y - (x + y)^(-1)*y^2 + (x + y)^(-1)*x^2').toString(), '-2*y');
test.equal(expr('x + x*y)/x').toString(), '1 + y');
test.equal(expr('f(x) + y*f(x))/f(x)').toString(), '1 + y');
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.equal(expr('x/-y)').toString(), '-x/y');
test.equal(expr('-x/0)').toString(), 'Infinity');
test.equal(expr('-a*x/(-y - b))').toString(), 'a*x/(b + y)');
test.done();
});
tape('Numeric equality checking', function(test) {
test.ok(numEquals('x + y', 'y + x'));
test.ok(numEquals('x + y*z^(w+1)', '(y / z^2) * z^(w+3) + x * 1'));
test.end();
});*/

@@ -20,9 +20,5 @@ // =============================================================================

test.equal(expr('aa + bb + cc').toString(), 'aa + bb + cc');
// test.equal(expr('5x + 2').toString(), '5 * x + 2');
// test.equal(expr('a*(b + c)').toString(), '(a*b) + (a*c)');
// test.equal(expr('a + b*c^d').toString(), 'a + (b*(c^d))');
// test.equal(expr('a + (b*c)^d').toString(), 'a + ((b^d)*(c^d))');
// test.equal(expr('((a + b)*c)^d').toString(), 'a + ((b^d)*(c^d))');
// test.equal(expr('([{|a|}])').toString(), '|a|');
test.equal(expr('5x + 2').toString(), '5 x + 2');
test.equal(expr('|a|').toString(), 'abs(a)');
test.end();
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc