UNPKG

kas-npm

Version:

A lightweight JavaScript CAS for comparing expressions and equations. Forked from @Khan to use in NPM.

1,508 lines (1,238 loc) 111 kB
/* TODO(charlie): fix these lint errors (http://eslint.org/docs/rules): */ /* eslint-disable indent, no-undef, no-var, one-var, no-dupe-keys, no-new-func, no-redeclare, no-unused-vars, comma-dangle, max-len, prefer-spread, space-infix-ops, space-unary-ops */ const _ = require('underscore'); const KAS = require('./parser'); (function(KAS) { /* The node hierarcy is as follows: (Expr) (Seq) 2+ children Add Mul Pow 2 children Log 2 children Eq 2 children Trig 1 child Abs 1 child (Symbol) Func 1 child e.g. f(x) Var leaf node e.g. x, x_n Const leaf node e.g. pi, e, <i> Unit leaf node e.g. kg (Num) leaf node Rational e.g. 2/3 Int Float (abstract, not meant to be instantiated) == Key design concepts == Functional: All methods return new nodes - nodes are never mutated. Ignore commutativity: Commutative inputs should be parsed equivalently. Exploit commutativity: Output should take advantage of ordering. */ /* non user-facing functions */ // assert that all abstract methods have been overridden var abstract = function() { // Try to give people a bit of information when this happens throw new Error("Abstract method - must override for expr: " + this.print()); }; // throw an error that is meant to be caught by the test suite (not user facing) var error = function(message) { throw new Error(message); }; // reliably detect NaN var isNaN = function(object) { return object !== object; }; // return a random float between min (inclusive) and max (exclusive), // not that inclusivity means much, probabilistically, on floats var randomFloat = function(min, max) { var extent = max - min; return Math.random() * extent + min; }; /* constants */ var ITERATIONS = 12; var TOLERANCE = 9; // decimal places /* abstract base expression node */ function Expr() {} _.extend(Expr.prototype, { // this node's immediate constructor func: abstract, // an array of the arguments to this node's immediate constructor args: abstract, // make a new node with the given arguments construct: function(args) { var instance = new this.func(); this.func.apply(instance, args); return instance; }, // an abstraction for chainable, bottom-up recursion recurse: function(method) { var passed = Array.prototype.slice.call(arguments, 1); var args = _.map(this.args(), function(arg) { return _.isString(arg) ? arg : arg[method].apply(arg, passed); }); return this.construct(args); }, // evaluate numerically with given variable mapping eval: abstract, codegen: abstract, compile: function() { var code = this.codegen(); try { return new Function("vars", "return " + code + ";"); } catch (e) { throw new Error("Function did not compile: " + code); } }, // returns a string unambiguously representing the expression // should be valid as input // e.g. this.equals(parse(this.print())) === true print: abstract, // returns a TeX string representing the expression tex: abstract, // returns a TeX string, modified by the given options asTex: function(options) { options = options || {}; _.defaults(options, { display: true, dynamic: true, times: false }); var tex = this.tex(); if (options.display) { tex = "\\displaystyle " + tex; } if (options.dynamic) { tex = tex.replace(/\(/g, "\\left("); tex = tex.replace(/\)/g, "\\right)"); } if (options.times) { tex = tex.replace(/\\cdot/g, "\\times"); } return tex; }, // returns the name of this expression's constructor as a string // only used for testing and debugging (the ugly regex is for IE8) name: function() { if (this.func.name) { return this.func.name; } else { return this.func.toString().match(/^function\s*([^\s(]+)/)[1]; } }, // returns a string representing current node structure repr: function() { return this.name() + "(" + _.map(this.args(), function(arg) { return _.isString(arg) ? arg : arg.repr(); }).join(",") + ")"; }, // removes all negative signs strip: function() { return this.recurse("strip"); }, // canonically reorders all commutative elements normalize: function() { return this.recurse("normalize"); }, // expands the expression expand: function() { return this.recurse("expand"); }, // naively factors out like terms factor: function(options) { return this.recurse("factor", options); }, // collect all like terms collect: function(options) { return this.recurse("collect", options); }, // strict syntactic equality check equals: function(other) { return this.normalize().print() === other.normalize().print(); }, // expand and collect until the expression no longer changes simplify: function(options) { options = _.extend({ once: false }, options); // Attempt to factor and collect var step1 = this.factor(options); var step2 = step1.collect(options); // Rollback if collect didn't do anything if (step1.equals(step2)) { step2 = this.collect(options); } // Attempt to expand and collect var step3 = step2.expand(options); var step4 = step3.collect(options); // Rollback if collect didn't do anything if (step3.equals(step4)) { step4 = step2.collect(options); } // One round of simplification complete var simplified = step4; if (options.once || this.equals(simplified)) { return simplified; } else { return simplified.simplify(options); } }, // check whether this expression is simplified isSimplified: function() { return this.equals(this.simplify()); }, // return the child nodes of this node exprArgs: function() { return _.filter(this.args(), function(arg) { return arg instanceof Expr; }); }, // return the variables (function and non) within the expression getVars: function(excludeFunc) { return _.uniq(_.flatten(_.invoke(this.exprArgs(), "getVars", excludeFunc))).sort(); }, getConsts: function() { return _.uniq(_.flatten(_.invoke(this.exprArgs(), "getConsts"))).sort(); }, getUnits: function() { return _.flatten(_.invoke(this.exprArgs(), "getUnits")); }, // check whether this expression node is of a particular type is: function(func) { return this instanceof func; }, // check whether this expression has a particular node type has: function(func) { if (this instanceof func) { return true; } return _.any(this.exprArgs(), function(arg) { return arg.has(func); }); }, // raise this expression to a given exponent // most useful for eventually implementing i^3 = -i, etc. raiseToThe: function(exp) { return new Pow(this, exp); }, // does this expression have a specific rendering hint? // rendering hints are picked up while parsing, but are lost during transformations isSubtract: function() { return false; }, isDivide: function() { return false; }, isRoot: function() { return false; }, // whether this node needs an explicit multiplication sign if following a Num needsExplicitMul: function() { return this.args()[0].needsExplicitMul(); }, // check that the variables in both expressions are the same sameVars: function(other) { var vars1 = this.getVars(); var vars2 = other.getVars(); // the other Expr can have more variables than this one // this lets you multiply equations by other variables var same = function(array1, array2) { return !_.difference(array1, array2).length; }; var lower = function(array) { return _.uniq(_.invoke(array, "toLowerCase")).sort(); }; var equal = same(vars1, vars2); var equalIgnoringCase = same(lower(vars1), lower(vars2)); return {equal: equal, equalIgnoringCase: equalIgnoringCase}; }, // semantic equality check, call after sameVars() to avoid potential false positives // plug in random numbers for the variables in both expressions // if they both consistently evaluate the same, then they're the same compare: function(other) { // equation comparisons are handled by Eq.compare() if (other instanceof Eq) { return false; } var varList = _.union( this.getVars(/* excludeFunc */ true), other.getVars(/* excludeFunc */ true)); // If the numbers are large we would like to do a relative comparison // rather than an absolute one, but if they're small enough then an // absolute comparison makes more sense var getDelta = function(num1, num2) { if (Math.abs(num1) < 1 || Math.abs(num2) < 1) { return Math.abs(num1 - num2); } else { return Math.abs(1 - num1 / num2); } }; var equalNumbers = function(num1, num2) { var delta = getDelta(num1, num2); return ((num1 === num2) || /* needed if either is +/- Infinity */ (isNaN(num1) && isNaN(num2)) || (delta < Math.pow(10, -TOLERANCE))); }; // if no variables, only need to evaluate once if (!varList.length && !this.has(Unit) && !other.has(Unit)) { return equalNumbers(this.eval(), other.eval()); } // collect here to avoid sometimes dividing by zero, and sometimes not // it is better to be deterministic, e.g. x/x -> 1 // TODO(alex): may want to keep track of assumptions as they're made var expr1 = this.collect(); var expr2 = other.collect(); var unitList1 = this.getUnits(); var unitList2 = other.getUnits(); if (!_.isEqual(unitList1, unitList2)) { return false; } // Compare at a set number (currently 12) of points to determine // equality. // // `range` (and `vars`) is the only variable that varies through the // iterations. For each of range = 10, 100, and 1000, each random // variable is picked from (-range, range). // // Note that because there are 12 iterations and three ranges, each // range is checked four times. for (var i = 0; i < ITERATIONS; i++) { var vars = {}; // One third total iterations each with range 10, 100, and 1000 var range = Math.pow(10, 1 + Math.floor(3 * i / ITERATIONS)); // Half of the iterations should only use integer values. // This is because expressions like (-2)^x are common but result // in NaN when evaluated in JS with non-integer values of x. // Without this, (-2)^x and (-2)^(x+1) both end up always being NaN // and thus equivalent. With this, the most common failure case is // avoided. However, less common cases such as (-2)^(x+0.1) and // (-2)^(x+1.1) will still both evaluate to NaN and result in a // false positive. // // Note that the above is only true in vanilla JS Number-land, // which has no concept of complex numbers. The solution is simple: // Integrate a library for handling complex numbers. // // TODO(alex): Add support for complex numbers, then remove this. var useFloats = i % 2 === 0; _.each(varList, function(v) { vars[v] = useFloats ? randomFloat(-range, range) : _.random(-range, range); }); var equal; if (expr1.has(Func) || expr2.has(Func) || expr1.has(Unit) || expr2.has(Unit)) { var result1 = expr1.partialEval(vars); var result2 = expr2.partialEval(vars); equal = result1.simplify().equals(result2.simplify()); } else { var result1 = expr1.eval(vars); var result2 = expr2.eval(vars); equal = equalNumbers(result1, result2); } if (!equal) { return false; } } return true; }, // evaluate as much of the expression as possible partialEval: function(vars) { if (this instanceof Unit) { return this; } else if (!this.has(Func)) { return new Float(this.eval(vars).toFixed(TOLERANCE)).collect(); } else if (this instanceof Func) { return new Func(this.symbol, this.arg.partialEval(vars)); } else { return this.recurse("partialEval", vars); } }, // check that the structure of both expressions is the same // all negative signs are stripped and the expressions are converted to // a canonical commutative form // should only be done after compare() returns true to avoid false positives sameForm: function(other) { return this.strip().equals(other.strip()); }, // returns the GCD of this expression and the given factor findGCD: function(factor) { return this.equals(factor) ? factor : Num.One; }, // return this expression's denominator getDenominator: function() { return Num.One; }, // return this expression as a Mul asMul: function() { return new Mul(Num.One, this); }, // TODO(alex): rename to isDefinitePositive or similar? // return whether this expression is 100% positive isPositive: abstract, // TODO(alex): rename to hasNegativeSign or similar? // return whether this expression has a negative sign isNegative: function() { return false; }, // return a factor of this expression that is 100% positive asPositiveFactor: function() { return this.isPositive() ? this : Num.One; }, // return a copy of the expression with a new hint set (preserves hints) addHint: function(hint) { if (!hint) { return this; } var expr = this.construct(this.args()); expr.hints = _.clone(this.hints); expr.hints[hint] = true; return expr; }, hints: { parens: false }, // currently unused! asExpr: function() { return this; }, // complete parse by performing a few necessary transformations completeParse: function() { return this.recurse("completeParse"); }, abs: abstract, negate: function() { return new Mul(Num.Neg, this); } }); /* abstract sequence node */ function Seq() {} Seq.prototype = new Expr(); _.extend(Seq.prototype, { args: function() { return this.terms; }, normalize: function() { var terms = _.sortBy(_.invoke(this.terms, "normalize"), function(term) { return term.print(); }); return new this.func(terms); }, expand: function() { return this.recurse("expand").flatten(); }, // partition the sequence into its numeric and non-numeric parts // makes no guarantees about the validity of either part! partition: function() { var terms = _.groupBy(this.terms, function(term) { return term instanceof Num; }); // XXX using a boolean as a key just converts it to a string. I don't // think this code was written with that in mind. Probably doesn't // matter except for readability. var numbers = terms[true] || []; var others = terms[false] || []; return [new this.func(numbers), new this.func(others)]; }, // ensure that sequences have 2+ terms and no nested sequences of the same type // this is a shallow flattening and will return a non-Seq if terms.length <= 1 flatten: function() { var type = this; var terms = _.reject(this.terms, function(term) { return term.equals(type.identity); }); if (terms.length === 0) { return type.identity; } if (terms.length === 1) { return terms[0]; } var grouped = _.groupBy(terms, function(term) { return term instanceof type.func; }); // same contains the children which are Seqs of the same type as this Seq var same = grouped[true] || []; var others = grouped[false] || []; var flattened = others.concat(_.flatten(_.pluck(same, "terms"), /* shallow: */ true)); return new type.func(flattened); }, // the identity associated with the sequence identity: undefined, // reduce a numeric sequence to a Num reduce: abstract, isPositive: function() { var terms = _.invoke(this.terms, "collect"); return _.all(_.invoke(terms, "isPositive")); }, // return a new Seq with a given term replaced by a different term // (or array of terms). given term can be passed directly, or by index // if no new term is provided, the old one is simply removed replace: function(oldTerm, newTerm) { var index; if (oldTerm instanceof Expr) { index = _.indexOf(this.terms, oldTerm); } else { index = oldTerm; } var newTerms = []; if (_.isArray(newTerm)) { newTerms = newTerm; } else if (newTerm) { newTerms = [newTerm]; } var terms = this.terms.slice(0, index) .concat(newTerms) .concat(this.terms.slice(index + 1)); return new this.func(terms); }, // syntactic sugar for replace() remove: function(term) { return this.replace(term); }, getDenominator: function() { // TODO(alex): find and return LCM return new Mul(_.invoke(this.terms, "getDenominator")).flatten(); } }); /* sequence of additive terms */ function Add() { if (arguments.length === 1) { this.terms = arguments[0]; } else { this.terms = _.toArray(arguments); } } Add.prototype = new Seq(); _.extend(Add.prototype, { func: Add, eval: function(vars, options) { return _.reduce(this.terms, function(memo, term) { return memo + term.eval(vars, options); }, 0); }, codegen: function() { return _.map(this.terms, function(term) { return "(" + term.codegen() + ")"; }).join(" + ") || "0"; }, print: function() { return _.invoke(this.terms, "print").join("+"); }, tex: function() { var tex = ""; _.each(this.terms, function(term) { if (!tex || term.isSubtract()) { tex += term.tex(); } else { tex += "+" + term.tex(); } }); return tex; }, collect: function(options) { var terms = _.invoke(this.terms, "collect", options); // [Expr expr, Num coefficient] var pairs = []; _.each(terms, function(term) { if (term instanceof Mul) { var muls = term.partition(); pairs.push([muls[1].flatten(), muls[0].reduce(options)]); } else if (term instanceof Num) { pairs.push([Num.One, term]); } else { pairs.push([term, Num.One]); } }); // { (Expr expr).print(): [[Expr expr, Num coefficient]] } var grouped = _.groupBy(pairs, function(pair) { return pair[0].normalize().print(); }); var collected = _.compact(_.map(grouped, function(pairs) { var expr = pairs[0][0]; var sum = new Add(_.zip.apply(_, pairs)[1]); var coefficient = sum.reduce(options); return new Mul(coefficient, expr).collect(options); })); // TODO(alex): use the Pythagorean identity here // e.g. x*sin^2(y) + x*cos^2(y) -> x return new Add(collected).flatten(); }, // naively factor out anything that is common to all terms // if options.keepNegative is specified, won't factor out a common -1 factor: function(options) { options = _.extend({ keepNegative: false }, options); var terms = _.invoke(this.terms, "collect"); var factors; if (terms[0] instanceof Mul) { factors = terms[0].terms; } else { factors = [terms[0]]; } _.each(_.rest(this.terms), function(term) { factors = _.map(factors, function(factor) { return term.findGCD(factor); }); }); if (!options.keepNegative && this.isNegative()) { factors.push(Num.Neg); } factors = new Mul(factors).flatten().collect(); var remainder = _.map(terms, function(term) { return Mul.handleDivide(term, factors).simplify(); }); remainder = new Add(remainder).flatten(); return Mul.createOrAppend(factors, remainder).flatten(); }, reduce: function(options) { return _.reduce(this.terms, function(memo, term) { return memo.add(term, options); }, this.identity); }, needsExplicitMul: function() { return false; }, isNegative: function() { var terms = _.invoke(this.terms, "collect"); return _.all(_.invoke(terms, "isNegative")); }, negate: function() { return new Add(_.invoke(this.terms, "negate")); } }); /* sequence of multiplicative terms */ function Mul() { if (arguments.length === 1) { this.terms = arguments[0]; } else { this.terms = _.toArray(arguments); } } Mul.prototype = new Seq(); _.extend(Mul.prototype, { func: Mul, eval: function(vars, options) { return _.reduce(this.terms, function(memo, term) { return memo * term.eval(vars, options); }, 1); }, codegen: function() { return _.map(this.terms, function(term) { return "(" + term.codegen() + ")"; }).join(" * ") || "0"; }, print: function() { return _.map(this.terms, function(term) { return (term instanceof Add) ? "(" + term.print() + ")" : term.print(); }).join("*"); }, getUnits: function() { var tmUnits = _(this.terms) .chain() .map(function(term) { return term.getUnits(); }) .flatten() .value(); tmUnits.sort(function(a, b) { return a.unit < b.unit; }); return tmUnits; }, // since we don't care about commutativity, we can render a Mul any way we choose // so we follow convention: first any negatives, then any numbers, then everything else tex: function() { var cdot = " \\cdot "; var terms = _.groupBy(this.terms, function(term) { if (term.isDivide()) { return "inverse"; } else if (term instanceof Num) { return "number"; } else { return "other"; } }); var inverses = terms.inverse || []; var numbers = terms.number || []; var others = terms.other || []; var negatives = ""; var numerator; // check all the numbers to see if there is a rational we can extract, // since we would like 1/2x/y to come out as \frac{1}{2}\frac{x}{y}, // and not \frac{1x}{2y}. for (var i = 0; i < numbers.length; i++) { var isRational = numbers[i] instanceof Rational && !(numbers[i] instanceof Int); if (isRational && others.length > 0 && inverses.length > 0) { var withThisRemoved = numbers.slice(); withThisRemoved.splice(i, 1); var newTerms = withThisRemoved.concat(inverses).concat(others); return numbers[i].tex() + new Mul(newTerms).tex(); } } numbers = _.compact(_.map(numbers, function(term) { var hasDenom = (term instanceof Rational) && !(term instanceof Int); var shouldPushDown = !term.hints.fraction || inverses.length > 0; if (hasDenom && shouldPushDown) { // e.g. 3x/4 -> 3/4*x (internally) -> 3x/4 (rendered) inverses.push(new Pow(new Int(term.d), Num.Div)); var number = new Int(term.n); number.hints = term.hints; return _.any(term.hints) ? number : null; } else { return term; } })); if (numbers.length === 0 && others.length === 1) { // e.g. (x+y)/z -> \frac{x+y}{z} numerator = others[0].tex(); } else { var tex = ""; _.each(numbers, function(term) { if (term.hints.subtract && term.hints.entered) { negatives += "-"; tex += (tex ? cdot : "") + term.abs().tex(); } else if ((term instanceof Int) && (term.n === -1) && (term.hints.negate || term.hints.subtract)) { // e.g. -1*-1 -> --1 // e.g. -1*x -> -x negatives += "-"; } else { // e.g. 2*3 -> 2(dot)3 tex += (tex ? cdot : "") + term.tex(); } }); _.each(others, function(term) { if (term.needsExplicitMul()) { // e.g. 2*2^3 -> 2(dot)2^3 tex += (tex ? cdot : "") + term.tex(); } else if (term instanceof Add) { // e.g. (a+b)*c -> (a+b)c tex += "(" + term.tex() + ")"; } else { // e.g. a*b*c -> abc tex += term.tex(); } }); numerator = tex ? tex : "1"; } if (!inverses.length) { return negatives + numerator; } else { var denominator = new Mul(_.invoke(inverses, "asDivide")).flatten().tex(); return negatives + "\\frac{" + numerator + "}{" + denominator + "}"; } }, strip: function() { var terms = _.map(this.terms, function(term) { return term instanceof Num ? term.abs() : term.strip(); }); return new Mul(terms).flatten(); }, // expand numerator and denominator separately expand: function() { var isAdd = function(term) { return term instanceof Add; }; var isInverse = function(term) { return term instanceof Pow && term.exp.isNegative(); }; var isInverseAdd = function(term) { return isInverse(term) && isAdd(term.base); }; var mul = this.recurse("expand").flatten(); var hasAdd = _.any(mul.terms, isAdd); var hasInverseAdd = _.any(mul.terms, isInverseAdd); if (!(hasAdd || hasInverseAdd)) { return mul; } var terms = _.groupBy(mul.terms, isInverse); var normals = terms[false] || []; var inverses = terms[true] || []; if (hasAdd) { var grouped = _.groupBy(normals, isAdd); var adds = grouped[true] || []; var others = grouped[false] || []; // loop over each additive sequence var expanded = _.reduce(adds, function(expanded, add) { // loop over each expanded array of terms return _.reduce(expanded, function(temp, array) { // loop over each additive sequence's terms return temp.concat(_.map(add.terms, function(term) { return array.concat(term); })); }, []); }, [[]]); // join each fully expanded array of factors with remaining multiplicative factors var muls = _.map(expanded, function(array) { return new Mul(others.concat(array)).flatten(); }); normals = [new Add(muls)]; } if (hasInverseAdd) { var denominator = new Mul(_.invoke(inverses, "getDenominator")).flatten(); inverses = [new Pow(denominator.expand(), Num.Div)]; } return new Mul(normals.concat(inverses)).flatten(); }, factor: function(options) { var factored = this.recurse("factor", options).flatten(); if (! (factored instanceof Mul)) { return factored; } // Combine any factored out Rationals into one, but don't collect var grouped = _.groupBy(factored.terms, function(term) { return term instanceof Rational; }); // Could also accomplish this by passing a new option // e.g. return memo.mul(term, {autocollect: false}); // TODO(alex): Decide whether this is a good use of options or not var rational = _.reduce(grouped[true], function(memo, term) { return {n: memo.n * term.n, d: memo.d * term.d}; }, {n: 1, d: 1}); if (rational.d === 1) { rational = new Int(rational.n); } else { rational = new Rational(rational.n, rational.d); } return new Mul((grouped[false] || []).concat(rational)).flatten(); }, collect: function(options) { var partitioned = this.recurse("collect", options).partition(); var number = partitioned[0].reduce(options); // e.g. 0*x -> 0 if (number.eval() === 0) { return Num.Zero; } var others = partitioned[1].flatten(); // e.g. 2*2 -> 4 // e.g. 2*2*x -> 4*x if (!(others instanceof Mul)) { return new Mul(number, others).flatten(); } others = others.terms; // [Expr base, Expr exp] var pairs = []; _.each(others, function(term) { if (term instanceof Pow) { pairs.push([term.base, term.exp]); } else { pairs.push([term, Num.One]); } }); // {(Expr base).print(): [[Expr base, Expr exp]]} var grouped = _.groupBy(pairs, function(pair) { return pair[0].normalize().print(); }); // [[Expr base, Expr exp]] var summed = _.compact(_.map(grouped, function(pairs) { var base = pairs[0][0]; var sum = new Add(_.zip.apply(_, pairs)[1]); var exp = sum.collect(options); if (exp instanceof Num && exp.eval() === 0) { return null; } else { return [base, exp]; } })); // XXX `pairs` is shadowed four or five times in this function var pairs = _.groupBy(summed, function(pair) { if (pair[0] instanceof Trig && pair[0].isBasic()) { return "trig"; } else if (pair[0] instanceof Log) { return "log"; } else { return "expr"; } }); var trigs = pairs.trig || []; var logs = pairs.log || []; var exprs = pairs.expr || []; if (trigs.length > 1) { // combine sines and cosines into other trig functions // {Trig.arg.print(): [[Trig base, Expr exp]]} var byArg = _.groupBy(trigs, function(pair) { return pair[0].arg.normalize().print(); }); trigs = []; _.each(byArg, function(pairs) { var arg = pairs[0][0].arg; // {Trig.type: Expr exp} var funcs = {sin: Num.Zero, cos: Num.Zero}; _.each(pairs, function(pair) { funcs[pair[0].type] = pair[1]; }); if (Mul.handleNegative(funcs.sin).collect(options).equals(funcs.cos)) { // e.g. sin^x(y)/cos^x(y) -> tan^x(y) if (funcs.cos.isNegative()) { funcs = {tan: funcs.sin}; } else { funcs = {cot: funcs.cos}; } } // TODO(alex): combine even if exponents not a perfect match // TODO(alex): transform 1/sin and 1/cos into csc and sec _.each(funcs, function(exp, type) { trigs.push([new Trig(type, arg), exp]); }); }); } if (logs.length > 1) { // combine logs with the same base // {Log.base.print(): [[Log base, Expr exp]]} var byBase = _.groupBy(logs, function(pair) { return pair[0].base.normalize().print(); }); logs = []; _.each(byBase, function(pairs) { // only combine two logs of the same base, otherwise commutative // differences result in different equally valid output // e.g. ln(x)/ln(z)*ln(y) -> log_z(x)*ln(y) // e.g. ln(x)*ln(y)/ln(z) -> ln(x)*log_z(y) if (pairs.length === 2 && Mul.handleNegative(pairs[0][1]).collect(options).equals(pairs[1][1])) { // e.g. ln(x)^y/ln(b)^y -> log_b(x)^y if (pairs[0][1].isNegative()) { logs.push([new Log(pairs[0][0].power, pairs[1][0].power), pairs[1][1]]); } else { logs.push([new Log(pairs[1][0].power, pairs[0][0].power), pairs[0][1]]); } } else { logs = logs.concat(pairs); } }); // TODO(alex): combine if all inverses are the same e.g. ln(y)*ln(z)/ln(x)/ln(x) } pairs = trigs.concat(logs).concat(exprs); var collected = _.map(pairs, function(pair) { return new Pow(pair[0], pair[1]).collect(options); }); return new Mul([number].concat(collected)).flatten(); }, isSubtract: function() { return _.any(this.terms, function(term) { return term instanceof Num && term.hints.subtract; }); }, // factor a single -1 in to the Mul // combine with a Num if all Nums are positive, else add as a term factorIn: function(hint) { var partitioned = this.partition(); var numbers = partitioned[0].terms; var fold = numbers.length && _.all(numbers, function(num) { return num.n > 0; }); if (fold) { // e.g. - x*2*3 -> x*-2*3 var num = numbers[0].negate(); num.hints = numbers[0].hints; return this.replace(numbers[0], num.addHint(hint)); } else { // e.g. - x*y -> -1*x*y // e.g. - x*-2 -> -1*x*-2 return new Mul([Num.negativeOne(hint)].concat(this.terms)); } }, // factor out a single hinted -1 (assume it is the division hint) // TODO(alex): make more general or rename to be more specific factorOut: function() { var factored = false; var terms = _.compact(_.map(this.terms, function(term, i, list) { if (!factored && term instanceof Num && term.hints.divide) { factored = true; return term.n !== -1 ? term.negate() : null; } else { return term; } })); if (terms.length === 1) { return terms[0]; } else { return new Mul(terms); } }, reduce: function(options) { return _.reduce(this.terms, function(memo, term) { return memo.mul(term, options); }, this.identity); }, findGCD: function(factor) { return new Mul(_.invoke(this.terms, "findGCD", factor)).flatten(); }, asMul: function() { return this; }, asPositiveFactor: function() { if (this.isPositive()) { return this; } else { var terms = _.invoke(this.collect().terms, "asPositiveFactor"); return new Mul(terms).flatten(); } }, isNegative: function() { return _.any(_.invoke(this.collect().terms, "isNegative")); }, fold: function() { return Mul.fold(this); }, negate: function() { var isNum = function(expr) { return expr instanceof Num; }; if (_.any(this.terms, isNum)) { var num = _.find(this.terms, isNum); return this.replace(num, num.negate()); } else { return new Mul([Num.Neg].concat(this.terms)); } } }); // static methods for the sequence types _.each([Add, Mul], function(type) { _.extend(type, { // create a new sequence unless left is already one (returns a copy) createOrAppend: function(left, right) { if (left instanceof type) { return new type(left.terms.concat(right)); } else { return new type(left, right); } } }); }); _.extend(Mul, { // negative signs should be folded into numbers whenever possible // never fold into a Num that's already negative or a Mul that has a negative Num // an optional hint is kept track of to properly render user input // an empty hint means negation handleNegative: function(expr, hint) { if (expr instanceof Num && expr.n > 0) { // e.g. - 2 -> -2 var negated = expr.negate(); // TODO(alex): rework hint system so that this isn't necessary negated.hints = expr.hints; return negated.addHint(hint); } else if (expr instanceof Mul) { // e.g. - x*2*3 -> x*-2*3 // e.g. - x*y -> -1*x*y // e.g. - x*-2 -> -1*x*-2 return expr.factorIn(hint); } else { // e.g. - x -> -1*x return new Mul(Num.negativeOne(hint), expr); } }, // division can create either a Rational or a Mul handleDivide: function(left, right) { // dividing by a Mul is the same as repeated division by its terms if (right instanceof Mul) { var first = Mul.handleDivide(left, right.terms[0]); var rest = new Mul(_.rest(right.terms)).flatten(); return Mul.handleDivide(first, rest); } var isInt = function(expr) { return expr instanceof Int; }; var isRational = function(expr) { return expr instanceof Rational; }; // for simplification purposes, fold Ints into Rationals if possible // e.g. 3x / 4 -> 3/4 * x (will still render as 3x/4) if (isInt(right) && left instanceof Mul && _.any(left.terms, isInt)) { // search from the right var reversed = left.terms.slice().reverse(); var num = _.find(reversed, isRational); if (!isInt(num)) { return new Mul(left.terms.concat([new Rational(1, right.n).addHint("fraction")])); } var rational = new Rational(num.n, right.n); rational.hints = num.hints; // in the case of something like 1/3 * 6/8, we want the // 6/8 to be considered a fraction, not just a division if (num === reversed[0]) { rational = rational.addHint("fraction"); } var result; if (num.n < 0 && right.n < 0) { rational.d = -rational.d; return left.replace(num, [Num.Neg, rational]); } else { return left.replace(num, rational); } } var divide = function(a, b) { if (b instanceof Int) { if (a instanceof Int) { if (a.n < 0 && b.n < 0) { // e.g. -2 / -3 -> -1*-2/3 return [Num.Neg, new Rational(a.n, -b.n).addHint("fraction")]; } else { // e.g. 2 / 3 -> 2/3 // e.g. -2 / 3 -> -2/3 // e.g. 2 / -3 -> -2/3 return [new Rational(a.n, b.n).addHint("fraction")]; } } else { // e.g. x / 3 -> x*1/3 // e.g. x / -3 -> x*-1/3 var inverse = new Rational(1, b.eval()); if (b.eval() < 0) { return [a, inverse.addHint("negate")]; } else { return [a, inverse]; } } } else { var pow; if (b instanceof Trig && b.exp) { // e.g. sin^2(x) -> sin(x)^2 var exp = b.exp; b.exp = undefined; b = new Pow(b, exp); } if (b instanceof Pow) { // e.g. (x^2) ^ -1 -> x^-2 // e.g. (x^y) ^ -1 -> x^(-1*y) // e.g. (x^(yz)) ^ -1 -> x^(-1*y*z) pow = new Pow(b.base, Mul.handleNegative(b.exp, "divide")); } else { // e.g. x ^ -1 -> x^-1 pow = new Pow(b, Num.Div); } if (a instanceof Int && a.n === 1) { // e.g. 1 / x -> x^-1 return [pow]; } else { // e.g. 2 / x -> 2*x^-1 return [a, pow]; } } }; if (left instanceof Mul) { var divided = divide(_.last(left.terms), right); return new Mul(_.initial(left.terms).concat(divided)); } else { var divided = divide(left, right); return new Mul(divided).flatten(); } }, // fold negative signs into numbers if possible // negative signs are not the same as multiplying by negative one! // e.g. -x -> -1*x simplified // e.g. -2*x -> -2*x simplified // e.g. -x*2 -> -1*x*2 not simplified -> x*-2 simplified // e.g. -1*x*2 -> -1*x*2 not simplified // also fold multiplicative terms into open Trig and Log nodes // e.g. (sin x)*x -> sin(x)*x // e.g. sin(x)*x -> sin(x)*x // e.g. sin(x)*(x) -> sin(x)*x // e.g. sin(x)*sin(y) -> sin(x)*sin(y) fold: function(expr) { if (expr instanceof Mul) { // assuming that this will be second to last var trigLog = _.find(_.initial(expr.terms), function(term) { return (term instanceof Trig || term instanceof Log) && term.hints.open; }); var index = _.indexOf(expr.terms, trigLog); if (trigLog) { var last = _.last(expr.terms); if (trigLog.hints.parens || last.hints.parens || last.has(Trig) || last.has(Log)) { trigLog.hints.open = false; } else { var newTrigLog; if (trigLog instanceof Trig) { newTrigLog = Trig.create([trigLog.type, trigLog.exp], Mul.createOrAppend(trigLog.arg, last).fold()); } else { newTrigLog = Log.create(trigLog.base, Mul.createOrAppend(trigLog.power, last).fold()); } if (index === 0) { return newTrigLog; } else { return new Mul(expr.terms.slice(0, index).concat(newTrigLog)).fold(); } } } var partitioned = expr.partition(); var numbers = partitioned[0].terms; var pos = function(num) { return num.n > 0; }; var neg = function(num) { return num.n === -1 && num.hints.negate; }; var posOrNeg = function(num) { return pos(num) || neg(num); }; if (numbers.length > 1 && _.some(numbers, neg) && _.some(numbers, pos) && _.every(numbers, posOrNeg)) { var firstNeg = _.indexOf(expr.terms, _.find(expr.terms, neg)); var firstNum = _.indexOf(expr.terms, _.find(expr.terms, pos)); // e.g. -x*2 -> x*-2 if (firstNeg < firstNum) { return expr.replace(firstNum, expr.terms[firstNum].negate()) .remove(firstNeg); } } } // in all other cases, make no change return expr; } }); /* exponentiation */ function Pow(base, exp) { this.base = base; this.exp = exp; } Pow.prototype = new Expr(); _.extend(Pow.prototype, { func: Pow, args: function() { return [this.base, this.exp]; }, eval: function(vars, options) { var evaledBase = this.base.eval(vars, options); var evaledExp = this.exp.eval(vars, options); // Math.pow unequivocally returns NaN when provided with both a // negative base and a fractional exponent. However, in some cases, we // know that our exponent is actually valid for use with negative // bases (e.g., (-5)^(1/3)). // // Here, we explicitly check for such cases. We really only handle a // limited subset (by requiring that the exponent is rational with an // odd denominator), but it's still useful. // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow if (evaledBase < 0) { var simplifiedExp = this.exp.simplify(); // If Float, convert to a Rational to enable the logic below if (simplifiedExp instanceof Float) { var num = simplifiedExp.n; var decimals = (num - num.toFixed()).toString().length - 2; var denominator = Math.pow(10, decimals); var rationalExp = new Rational(num * denominator, denominator); simplifiedExp = rationalExp.simplify(); } if (simplifiedExp instanceof Rational) { var oddDenominator = Math.abs(simplifiedExp.d) % 2 === 1; if (oddDenominator) { var oddNumerator = Math.abs(simplifiedExp.n) % 2 === 1; var sign = (oddNumerator) ? -1 : 1; return sign * Math.pow(-1 * evaledBase, evaledExp); } } } return Math.pow(evaledBase, evaledExp); }, getUnits: function() { return this.base.getUnits().map(function(unit) { return { unit: unit.unit, pow: unit.pow * this.exp.n }; }.bind(this)); }, codegen: function() { return "Math.pow(" + this.base.codegen() + ", " + this.exp.codegen() + ")"; }, print: function() { var base = this.base.print(); if (this.base instanceof Seq || this.base instanceof Pow) { base = "(" + base + ")"; } return base + "^(" + this.exp.print() + ")"; }, tex: function() { if (this.isDivide()) { // e.g. x ^ -1 w/hint -> 1/x return "\\frac{1}{" + this.asDivide().tex() + "}"; } else if (this.isRoot()) { if (this.exp.n !== 1) { error("Node marked with hint 'root' does not have exponent " + "of form 1/x."); } if (this.exp.d === 2) { // e.g. x ^ 1/2 w/hint -> sqrt{x} return "\\sqrt{" + this.base.tex() + "}"; } else { // e.g. x ^ 1/y w/hint -> sqrt[y]{x} return "\\sqrt[" + this.exp.d + "]{" + this.base.tex() + "}"; } } else if (this.base instanceof Trig && !this.base.isInverse() && this.exp instanceof Num && this.exp.isSimple() && this.exp.eval() >= 0) { // e.g sin(x) ^ 2 -> sin^2(x) var split = this.base.tex({split: true}); return split[0] + "^{" + this.exp.tex() + "}" + split[1]; } else { // e.g. x ^ y -> x^y var base = this.base.tex(); if (this.base instanceof Seq || this.base instanceof Pow || (this.base instanceof Num && !this.base.isSimple())) { // e.g. a+b ^ c -> (a+b)^c base = "(" + base + ")"; } else if (this.base instanceof Trig || this.base instanceof Log) { // e.g. ln(x) ^ 2 -> [ln(x)]^2 base = "[" + base + "]"; } return base + "^{" + this.exp.tex() + "}"; } }, needsExplicitMul: function() { return this.isRoot() ? false : this.base.needsExplicitMul(); }, expand: function() { var pow = this.recurse("expand"); if (pow.base instanceof Mul) {