Socket
Socket
Sign inDemoInstall

@mathigon/hilbert

Package Overview
Dependencies
Maintainers
1
Versions
56
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.1 to 0.2.1

src/elements.js

464

build/hilbert.js

@@ -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();
});
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