nerdamer-ts
Version:
javascript light-weight symbolic math expression evaluator
719 lines • 28.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = exports.parse = exports.ParseDeps = exports.Parser = void 0;
const Collection_1 = require("./Collection");
const Slice_1 = require("./Slice");
const Token_1 = require("./Token");
const Trig_1 = require("../Functions/Trig");
const Trig_hyperbolic_1 = require("../Functions/Trig.hyperbolic");
const Symbol_1 = require("../Types/Symbol");
const Settings_1 = require("../Settings");
const RPN_1 = require("./RPN");
const Utils_1 = require("../Core/Utils");
const Groups_1 = require("../Types/Groups");
const LaTeX_1 = require("../LaTeX/LaTeX");
const Vector_1 = require("../Types/Vector");
const Errors_1 = require("../Core/Errors");
const Core_1 = require("../Functions/Core");
//Uses modified Shunting-yard algorithm. http://en.wikipedia.org/wiki/Shunting-yard_algorithm
class Parser {
constructor(tokenizer, operators, functionProvider, variables, peekers, units) {
// exports for back compatibility
this.classes = {
Collection: Collection_1.Collection,
Slice: Slice_1.Slice,
Token: Token_1.Token
};
this.trig = Trig_1.Trig;
this.trigh = Trig_hyperbolic_1.TrigHyperbolic;
this.error = Errors_1.err;
this.symfunction = Symbol_1.symfunction;
this.bin = {};
// private
this.remove_redundant_powers = function (arr) {
// The filtered array
let narr = [];
while (arr.length) {
// Remove the element from the front
let e = arr.shift();
let next = arr[0];
let next_is_array = (0, Utils_1.isArray)(next);
let next_is_minus = next === '-';
// Remove redundant plusses
if (e === '^') {
if (next === '+') {
arr.shift();
}
else if (next_is_array && next[0] === '+') {
next.shift();
}
// Remove redundant parentheses
if (next_is_array && next.length === 1) {
arr.unshift(arr.shift()[0]);
}
}
// Check if it's a negative power
if (e === '^' && (next_is_array && next[0] === '-' || next_is_minus)) {
// If so:
// - Remove it from the new array, place a one and a division sign in that array and put it back
let last = narr.pop();
// Check if it's something multiplied by
let before = narr[narr.length - 1];
let before_last = '1';
if (before === '*') {
narr.pop();
// For simplicity we just pop it.
before_last = narr.pop();
}
// Implied multiplication
else if ((0, Utils_1.isArray)(before)) {
before_last = narr.pop();
}
narr.push(before_last, '/', last, e);
// Remove the negative sign from the power
if (next_is_array) {
next.shift();
}
else {
arr.shift();
}
// Remove it from the array so we don't end up with redundant parentheses if we can
if (next_is_array && next.length === 1) {
narr.push(arr.shift()[0]);
}
}
else {
narr.push(e);
}
}
return narr;
};
this.tokenizer = tokenizer;
this.operators = operators;
this.functionProvider = functionProvider;
this.variables = variables;
this.peekers = peekers;
this.units = units;
operators.injectOperatorsDeps({
registerOperator: (name, operation) => this.setAction(name, operation)
});
}
getAction(name) {
// return this.actions[name];
return this[name];
}
setAction(name, func) {
// this.actions[name] = func;
this[name] = func;
}
setOperator(operator, action = undefined, shift = undefined) {
this.operators.setOperator(operator, action, shift);
}
//delay setting of constants until Settings is ready
initConstants() {
this.variables.setConstant('E', new Symbol_1.Symbol(Settings_1.Settings.E));
this.variables.setConstant('PI', new Symbol_1.Symbol(Settings_1.Settings.PI));
}
parse(e, substitutions = {}) {
let tokens = this.tokenizer.tokenize(e, true);
let rpn = this.toRPN(tokens);
return this.parseRPN(rpn, substitutions);
}
/**
* Tokenizes the string
* @param {String} e
* @returns {Token[]}
*/
tokenize(e) {
return this.tokenizer.tokenize(e, false);
}
/**
* Puts token array in Reverse Polish Notation
* @param {Token[]} tokens
* @returns {Token[]}
*/
toRPN(tokens) {
return RPN_1.RPN.TokensToRPN(tokens);
}
/**
* Parses the tokens
* @param {Token[]} rpn
* @param {object} substitutions
* @returns {Symbol}
*/
parseRPN(rpn, substitutions) {
let rpnDeps = {
callfunction: (...args) => this.callfunction(...args),
getAction: (action) => {
return this.getAction(action).bind(this);
}
};
let rpnParser = new RPN_1.RPN(rpnDeps, this.variables, this.peekers);
return rpnParser.parseRPN(rpn, substitutions);
}
/**
* This method is supposed to behave similarly to the override method but it does not override
* the existing function rather it only extends it
* @param {String} what
* @param {Function} with_what
* @param {boolean} force_call
*/
extend(what, with_what, force_call) {
let extended = this.getAction(what);
if (typeof extended === 'function' && typeof with_what === 'function') {
let f = extended;
this.setAction(what, (a, b) => {
if ((0, Utils_1.isSymbol)(a) && (0, Utils_1.isSymbol)(b) && !force_call) {
return f.call(this, a, b);
}
else {
return with_what.call(this, a, b, f);
}
});
}
}
/**
* This method gives the ability to override operators with new methods.
* @param {String} which
* @param {Function} with_what
*/
override(which, with_what) {
if (!this.bin[which]) {
this.bin[which] = [];
}
this.bin[which].push(this[which]);
this[which] = with_what;
}
/**
* Restores a previously overridden operator
* @param {String} what
*/
restore(what) {
if (this[what]) {
this[what] = this.bin[what].pop();
}
}
clean(symbol) {
// handle functions with numeric values
// handle denominator within denominator
// handle trig simplifications
let g = symbol.group, retval;
//Now let's get to work
if (g === Groups_1.Groups.CP) {
let num = symbol.getNum(), den = symbol.getDenom() || new Symbol_1.Symbol(1), p = Number(symbol.power), factor = new Symbol_1.Symbol(1);
if (Math.abs(p) === 1) {
den.each(x => {
if (x.group === Groups_1.Groups.CB) {
factor = (0, Core_1.multiply)(factor, this.clean(x.getDenom()));
}
else if (x.power.lessThan(0)) {
factor = (0, Core_1.multiply)(factor, this.clean(x.clone().toUnitMultiplier()));
}
});
let new_den = new Symbol_1.Symbol(0);
//now divide out the factor and add to new den
den.each(function (x) {
new_den = (0, Core_1.add)((0, Core_1.divide)(x, factor.clone()), new_den);
});
factor.invert(); //invert so it can be added to the top
let new_num;
if (num.isComposite()) {
new_num = new Symbol_1.Symbol(0);
num.each(x => {
new_num = (0, Core_1.add)((0, Core_1.multiply)(this.clean(x), factor.clone()), new_num);
});
}
else
new_num = (0, Core_1.multiply)(factor, num);
retval = (0, Core_1.divide)(new_num, new_den);
}
}
else if (g === Groups_1.Groups.CB) {
retval = new Symbol_1.Symbol(1);
symbol.each(x => {
retval = (0, Core_1.multiply)(retval, this.clean(x));
});
}
else if (g === Groups_1.Groups.FN) {
if (symbol.args.length === 1 && symbol.args[0].isConstant())
retval = (0, Utils_1.block)('PARSE2NUMBER', () => {
return this.parse(symbol);
}, true);
}
if (!retval)
retval = symbol;
return retval;
}
/**
* An internal function call for the Parser. This will either trigger a real
* function call if it can do so or just return a symbolic representation of the
* function using symfunction.
* @param {String} fn_name
* @param {Array} args
* @param {int} allowed_args
* @returns {Symbol}
*/
callfunction(fn_name, args, allowed_args = undefined) {
let fn_settings = this.functionProvider.getFunctionDescriptor(fn_name);
if (!fn_settings)
(0, Errors_1.err)('Nerdamer currently does not support the function ' + fn_name);
let num_allowed_args = fn_settings[1] || allowed_args, //get the number of allowed arguments
fn = fn_settings[0], //get the mapped function
retval;
//We want to be able to call apply on the arguments or create a symfunction. Both require
//an array so make sure to wrap the argument in an array.
if (!(args instanceof Array))
args = args !== undefined ? [args] : [];
if (num_allowed_args !== -1) {
let is_array = (0, Utils_1.isArray)(num_allowed_args), min_args = is_array ? num_allowed_args[0] : num_allowed_args, max_args = is_array ? num_allowed_args[1] : num_allowed_args, num_args = args.length;
let error_msg = fn_name + ' requires a {0} of {1} arguments. {2} provided!';
if (num_args < min_args)
(0, Errors_1.err)((0, Utils_1.format)(error_msg, 'minimum', min_args, num_args));
if (num_args > max_args)
(0, Errors_1.err)((0, Utils_1.format)(error_msg, 'maximum', max_args, num_args));
}
/*
* The following are very important to the how nerdamer constructs functions!
* Assumption 1 - if fn is undefined then handling of the function is purely numeric. This
* enables us to reuse Math, Math2, ..., any function from Settings.FUNCTIONS_MODULES entry
* Assumption 2 - if fn is defined then that function takes care of EVERYTHING including symbolics
* Assumption 3 - if the user calls symbolics on a function that returns a numeric value then
* they are expecting a symbolic output.
*/
//check if arguments are all numers
let numericArgs = (0, Utils_1.allNumbers)(args);
//Big number support. Check if Big number is requested and the arguments are all numeric and, not imaginary
// if (Settings.USE_BIG && numericArgs) {
// retval = Big[fn_name].apply(undefined, args);
// }
// else {
if (!fn) {
//Remember assumption 1. No function defined so it MUST be numeric in nature
fn = this.functionProvider.findFunction(fn_name);
if (Settings_1.Settings.PARSE2NUMBER && numericArgs)
retval = (0, Symbol_1.bigConvert)(fn.apply(fn, args));
else
retval = (0, Symbol_1.symfunction)(fn_name, args);
}
else {
//Remember assumption 2. The function is defined so it MUST handle all aspects including numeric values
let thisArg = fn_settings[2] || {};
thisArg.parser = this;
retval = fn.apply(thisArg, args);
}
// }
return retval;
}
;
//TODO: Utilize the function below instead of the linked function
getFunction(name) {
return this.functionProvider.getFunctionDescriptors(name)[0];
}
/**
* TODO: Switch to Parser.tokenize for this method
* Reads a string into an array of Symbols and operators
* @param {String} expression_string
* @returns {Array}
*/
toObject(expression_string) {
let objectify = (tokens) => {
let output = [];
for (let i = 0, l = tokens.length; i < l; i++) {
let token = tokens[i];
let v = token.value;
if (token.type === Token_1.Token.VARIABLE_OR_LITERAL) {
output.push(new Symbol_1.Symbol(v));
}
else if (token.type === Token_1.Token.FUNCTION) {
//jump ahead since the next object are the arguments
i++;
//create a symbolic function and stick it on output
let f = (0, Symbol_1.symfunction)(v, objectify(tokens[i]));
f.isConversion = true;
output.push(f);
}
else if (token.type === Token_1.Token.OPERATOR) {
output.push(v);
}
else {
output.push(objectify(token));
}
}
return output;
};
return objectify(this.tokenize(expression_string));
}
// A helper method for toTeX
// private
chunkAtCommas(arr) {
let chunks = [[]];
for (let j = 0, k = 0, l = arr.length; j < l; j++) {
if (arr[j] === ',') {
k++;
chunks[k] = [];
}
else {
chunks[k].push(arr[j]);
}
}
return chunks;
}
// Helper method for toTeX
// private
rem_brackets(str) {
return str.replace(/^\\left\((.+)\\right\)$/g, function (str, a) {
if (a) {
return a;
}
return str;
});
}
/**
* Convert expression or object to LaTeX
* @param {string} expression_or_obj
* @param {ConvertToLaTeXOptions} opt
* @returns {string}
*/
toTeX(expression_or_obj, opt) {
opt = opt || {};
// Add decimal option as per issue #579. Consider passing an object to Latex.latex as option instead of string
let decimals = opt.decimals === true ? 'decimals' : undefined;
let obj = typeof expression_or_obj === 'string' ? this.toObject(expression_or_obj) : expression_or_obj, TeX = [], cdot = typeof opt.cdot === 'undefined' ? '\\cdot' : opt.cdot; //set omit cdot to true by default
// Remove negative powers as per issue #570
obj = this.remove_redundant_powers(obj);
if ((0, Utils_1.isArray)(obj)) {
let nobj = [], a, b;
//first handle ^
for (let i = 0; i < obj.length; i++) {
a = obj[i];
if (obj[i + 1] === '^') {
b = obj[i + 2];
nobj.push(LaTeX_1.LaTeX.braces(this.toTeX([a])) + '^' + LaTeX_1.LaTeX.braces(this.toTeX([b])));
i += 2;
}
else {
nobj.push(a);
}
}
obj = nobj;
}
for (let i = 0, l = obj.length; i < l; i++) {
let e = obj[i];
// Convert * to cdot
if (e === '*') {
e = cdot;
}
if ((0, Utils_1.isSymbol)(e)) {
if (e.group === Groups_1.Groups.FN) {
let fname = e.fname, f;
if (fname === Settings_1.Settings.SQRT)
f = '\\sqrt' + LaTeX_1.LaTeX.braces(this.toTeX(e.args));
else if (fname === Settings_1.Settings.ABS)
f = LaTeX_1.LaTeX.brackets(this.toTeX(e.args), 'abs');
else if (fname === Settings_1.Settings.PARENTHESIS)
f = LaTeX_1.LaTeX.brackets(this.toTeX(e.args), 'parens');
else if (fname === Settings_1.Settings.LOG10) {
f = '\\' + Settings_1.Settings.LOG10_LATEX + '\\left( ' + this.toTeX(e.args) + '\\right)';
}
else if (fname === 'integrate') {
/* Retrive [Expression, x] */
let chunks = this.chunkAtCommas(e.args);
/* Build TeX */
let expr = LaTeX_1.LaTeX.braces(this.toTeX(chunks[0])), dx = this.toTeX(chunks[1]);
f = '\\int ' + expr + '\\, d' + dx;
}
else if (fname === 'defint') {
let chunks = this.chunkAtCommas(e.args), expr = LaTeX_1.LaTeX.braces(this.toTeX(chunks[0])), dx = this.toTeX(chunks[3]), lb = this.toTeX(chunks[1]), ub = this.toTeX(chunks[2]);
f = '\\int\\limits_{' + lb + '}^{' + ub + '} ' + expr + '\\, d' + dx;
}
else if (fname === 'diff') {
let chunks = this.chunkAtCommas(e.args);
let dx = '', expr = LaTeX_1.LaTeX.braces(this.toTeX(chunks[0]));
/* Handle cases: one argument provided, we need to guess the variable, and assume n = 1 */
if (chunks.length === 1) {
let vars = [];
for (let j = 0; j < chunks[0].length; j++) {
if (chunks[0][j].group === 3) {
vars.push(chunks[0][j].value);
}
}
vars.sort();
dx = vars.length > 0 ? ('\\frac{d}{d ' + vars[0] + '}') : '\\frac{d}{d x}';
}
/* If two arguments, we have expression and variable, we assume n = 1 */
else if (chunks.length === 2) {
dx = '\\frac{d}{d ' + chunks[1] + '}';
}
/* If we have more than 2 arguments, we assume we've got everything */
else {
dx = '\\frac{d^{' + chunks[2] + '}}{d ' + this.toTeX(chunks[1]) + '^{' + chunks[2] + '}}';
}
f = dx + '\\left(' + expr + '\\right)';
}
else if (fname === 'sum' || fname === 'product') {
// Split e.args into 4 parts based on locations of , symbols.
let argSplit = [[], [], [], []], j = 0, i;
for (i = 0; i < e.args.length; i++) {
if (e.args[i] === ',') {
j++;
continue;
}
argSplit[j].push(e.args[i]);
}
// Then build TeX string.
f = (fname === 'sum' ? '\\sum_' : '\\prod_') + LaTeX_1.LaTeX.braces(this.toTeX(argSplit[1]) + ' = ' + this.toTeX(argSplit[2]));
f += '^' + LaTeX_1.LaTeX.braces(this.toTeX(argSplit[3])) + LaTeX_1.LaTeX.braces(this.toTeX(argSplit[0]));
}
else if (fname === 'limit') {
let args = this.chunkAtCommas(e.args).map(x => {
if (Array.isArray(x))
return this.toTeX(x.join(''));
return this.toTeX(String(x));
});
f = '\\lim_' + LaTeX_1.LaTeX.braces(args[1] + '\\to ' + args[2]) + ' ' + LaTeX_1.LaTeX.braces(args[0]);
}
else if (fname === Settings_1.Settings.FACTORIAL || fname === Settings_1.Settings.DOUBLEFACTORIAL)
f = this.toTeX(e.args) + (fname === Settings_1.Settings.FACTORIAL ? '!' : '!!');
else {
f = LaTeX_1.LaTeX.latex(e, decimals);
//f = '\\mathrm'+LaTeX.braces(fname.replace(/_/g, '\\_')) + LaTeX.brackets(this.toTeX(e.args), 'parens');
}
TeX.push(f);
}
else {
TeX.push(LaTeX_1.LaTeX.latex(e, decimals));
}
}
else if ((0, Utils_1.isArray)(e)) {
TeX.push(LaTeX_1.LaTeX.brackets(this.toTeX(e)));
}
else {
if (e === '/')
TeX.push(LaTeX_1.LaTeX.frac(this.rem_brackets(TeX.pop()), this.rem_brackets(this.toTeX([obj[++i]]))));
else
TeX.push(e);
}
}
return TeX.join(' ');
}
;
isOperator(name) {
return this.operators.isOperator();
}
getOperatorsClass() {
return this.operators;
}
getBrackets() {
return this.operators.getBrackets();
}
get functions() {
return this.getFunctions();
}
getFunctions() {
return this.functionProvider.getFunctionDescriptors();
}
getFunctionProvider() {
return this.functionProvider;
}
// Gets called when the parser finds the , operator.
// Commas return a Collector object which is roughly an array
comma(a, b) {
if (!(a instanceof Collection_1.Collection))
a = Collection_1.Collection.create(a);
a.append(b);
return a;
}
// Used to slice elements from arrays
slice(a, b) {
return new Slice_1.Slice(a, b);
}
// The equality setter
equals(a, b) {
// Equality can only be set for group S so complain it's not
if (a.group !== Groups_1.Groups.S && !a.isLinear()) {
(0, Errors_1.err)('Cannot set equality for ' + a.toString());
}
this.variables.setVar(a.value, b.clone());
return b;
}
// Percent
percent(a) {
return (0, Core_1.divide)(a, new Symbol_1.Symbol(100));
}
// Set variable
assign(a, b) {
if (a instanceof Collection_1.Collection && b instanceof Collection_1.Collection) {
a.elements.map((x, i) => {
return this.assign(x, b.elements[i]);
});
return Vector_1.Vector.fromArray(b.elements);
}
if (a.parent) {
// It's referring to the parent instead. The current item can be discarded
let e = a.parent;
e.elements[e.getter] = b;
delete e.getter;
return e;
}
if (a.group !== Groups_1.Groups.S) {
throw new Errors_1.NerdamerValueError('Cannot complete operation. Incorrect LH value for ' + a);
}
this.variables.setVar(a.value, b);
return b;
}
function_assign(a, b) {
let f = a.elements.pop();
return this.setFunction(f, a.elements, b);
}
// Function to quickly convert bools to Symbols
bool2Symbol(x) {
return new Symbol_1.Symbol(x === true ? 1 : 0);
}
//check for equality
eq(a, b) {
return this.bool2Symbol(a.equals(b));
}
//checks for greater than
gt(a, b) {
return this.bool2Symbol(a.gt(b));
}
//checks for greater than equal
gte(a, b) {
return this.bool2Symbol(a.gte(b));
}
//checks for less than
lt(a, b) {
return this.bool2Symbol(a.lt(b));
}
//checks for less than equal
lte(a, b) {
return this.bool2Symbol(a.lte(b));
}
// wraps the factorial
factorial(a) {
return (0, Symbol_1.symfunction)(Settings_1.Settings.FACTORIAL, [a]);
}
// wraps the double factorial
dfactorial(a) {
return (0, Symbol_1.symfunction)(Settings_1.Settings.DOUBLEFACTORIAL, [a]);
}
//Link the functions to the parse so they're available outside of the library.
//This is strictly for convenience and may be deprecated.
expand(symbol, opt = undefined) {
return (0, Core_1.expand)(symbol, opt);
}
round(x, s) {
return (0, Core_1.round)(x, s);
}
cbrt(symbol) {
return (0, Core_1.cbrt)(symbol);
}
abs(symbol) {
return (0, Core_1.abs)(symbol);
}
log(symbol, base) {
return (0, Core_1.log)(symbol, base);
}
rationalize(symbol) {
return (0, Core_1.rationalize)(symbol);
}
nthroot(num, p, prec, asbig) {
return (0, Core_1.nthroot)(num, p, prec, asbig);
}
arg(symbol) {
return (0, Core_1.arg)(symbol);
}
conjugate(symbol) {
return (0, Core_1.conjugate)(symbol);
}
imagpart(symbol) {
return (0, Core_1.imagpart)(symbol);
}
realpart(symbol) {
return (0, Core_1.realpart)(symbol);
}
sqrt(symbol) {
return (0, Core_1.sqrt)(symbol);
}
multiply(a, b) {
return (0, Core_1.multiply)(a, b);
}
divide(a, b) {
return (0, Core_1.divide)(a, b);
}
subtract(a, b) {
return (0, Core_1.subtract)(a, b);
}
add(a, b) {
return (0, Core_1.add)(a, b);
}
pow(a, b) {
return (0, Core_1.pow)(a, b);
}
mod(symbol1, symbol2) {
return (0, Core_1.mod)(symbol1, symbol2);
}
tree(expression) {
let tokens = this.tokenize(expression);
tokens = this.toRPN(tokens);
return this.tokenizer.tree(tokens);
}
setFunction(name, params_array, body) {
(0, Utils_1.validateName)(name);
if (!this.variables.isReserved(name)) {
params_array = params_array || this.parse(body).variables();
// The function gets set to PARSER.mapped function which is just
// a generic function call.
//The loader for functions which are not part of Math2
const mapped_function = function () {
let subs = {}, params = this.params;
for (let i = 0; i < params.length; i++) {
subs[params[i]] = String(arguments[i]);
}
return this.parser.parse(this.body, subs);
};
this.functionProvider.setFunctionDescriptor(name, [mapped_function, params_array.length, {
name: name,
params: params_array,
body: body
}]);
return body;
}
return null;
}
;
}
exports.Parser = Parser;
// type ParseDepsType = {
// parser: Parser | null;
// }
/**
*
* @type {{parser: Parser}}
*/
exports.ParseDeps = {
parser: null
};
/**
*
* @param {string | Symbol} e
* @param {object} substitutions
* @return {*}
*/
function parse(e, substitutions = {}) {
return exports.ParseDeps.parser.parse(e, substitutions);
}
exports.parse = parse;
/**
* As the name states. It forces evaluation of the expression
* @param {string} expression
* @param {Symbol} o
* @deprecated use Utils.evaluate instead
*/
function evaluate(expression, o = undefined) {
return (0, Utils_1.block)('PARSE2NUMBER', function () {
return parse(expression, o);
}, true);
}
exports.evaluate = evaluate;
//# sourceMappingURL=Parser.js.map