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
JavaScript
/* 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) {