diff --git a/.changeset/rare-pumas-rule.md b/.changeset/rare-pumas-rule.md new file mode 100644 index 0000000000..f98b766690 --- /dev/null +++ b/.changeset/rare-pumas-rule.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/kas": patch +--- + +Convert nodes.js to use TypeScript diff --git a/packages/kas/src/nodes.js b/packages/kas/src/nodes.ts similarity index 76% rename from packages/kas/src/nodes.js rename to packages/kas/src/nodes.ts index 9481c91d1c..f63d3fcacb 100644 --- a/packages/kas/src/nodes.js +++ b/packages/kas/src/nodes.ts @@ -18,7 +18,7 @@ import {parser} from "./__genfiles__/parser"; Eq 2 children Trig 1 child Abs 1 child - (Symbol) + (Sym) Func 1 child e.g. f(x) Var leaf node e.g. x, x_n Const leaf node e.g. pi, e, @@ -38,29 +38,14 @@ import {parser} from "./__genfiles__/parser"; /* 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: " + - // eslint-disable-next-line @babel/no-invalid-this - 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) { +const 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) { +const randomFloat = function (min: number, max: number) { var extent = max - min; return Math.random() * extent + min; }; @@ -69,8 +54,67 @@ var randomFloat = function (min, max) { var ITERATIONS = 12; var TOLERANCE = 9; // decimal places +// NOTE(kevinb): _.partition exists in a more recent version of underscore. +// To avoid having to update underscore I've added a hacky version of this +// method here. +function partition>( + list: V, + iteratee: _.CollectionIterator, +): [_.TypeOfCollection[], _.TypeOfCollection[]] { + const a: _.TypeOfCollection[] = []; + const b: _.TypeOfCollection[] = []; + _.forEach(list, (elem, key, ctx) => { + if (iteratee(elem, key, ctx)) { + a.push(elem); + } else { + b.push(elem); + } + }); + return [a, b]; +} + +function isExpr(arg: string | Expr): arg is Expr { + return arg instanceof Expr; +} + +const isAdd = function (term: Expr): term is Add { + return term instanceof Add; +}; + +function isRational(arg: Expr): arg is Rational { + return arg instanceof Rational; +} + +function getFactors(expr: Expr): Expr[] { + if (expr instanceof Mul) { + return expr.terms; + } else { + return [expr]; + } +} + +type Hints = { + parens?: boolean; + divide?: boolean; + root?: boolean; + subtract?: boolean; + fraction?: boolean; + entered?: boolean; + negate?: boolean; + open?: boolean; +}; + +type Vars = Record; + +type Options = { + preciseFloats?: boolean; + once?: boolean; +}; + /* abstract base expression node */ -class Expr { +abstract class Expr { + hints: Hints; + constructor() { this.hints = { parens: false, @@ -78,17 +122,16 @@ class Expr { } // this node's immediate constructor - func() { - abstract(); - } + // The `new (...args: any[]): any;` part of the type is a + // "construct" signature. It indicates that `func` is a class. + // See https://www.typescriptlang.org/docs/handbook/2/functions.html#construct-signatures. + abstract func: {new (...args: any[]): any; name: string}; // an array of the arguments to this node's immediate constructor - args() { - return abstract(); - } + abstract args(): (number | string | Expr)[]; // make a new node with the given arguments - construct(args) { + construct(args: any[]) { const func = this.func; const instance = new func(...args); if (typeof instance === "undefined") { @@ -98,25 +141,45 @@ class Expr { } // an abstraction for chainable, bottom-up recursion - recurse(method, ...passed) { + // NOTE(kevinb): This method is highly dynamic. It's possible that it + // could be made more type-safe using overload signatures. + recurse(method: string, ...passed: any[]): this { var args = _.map(this.args(), function (arg) { - return _.isString(arg) ? arg : arg[method].apply(arg, passed); + return _.isString(arg) || _.isNumber(arg) + ? arg + : arg[method].apply(arg, passed); }); return this.construct(args); } // evaluate numerically with given variable mapping - eval() { - return abstract(); + // NOTE(kevin): This could made into an abstract method but + // Eq doesn't implement it. This indicates that we probably + // need to introduce another class in our hierarchy. + eval(vars: Vars = {}, options?: ParseOptions): number { + throw new Error( + "Abstract method - must override for expr: " + + // eslint-disable-next-line @babel/no-invalid-this + this.print(), + ); } - codegen() { - return abstract(); + // NOTE(kevin): This could made into an abstract method but + // Eq doesn't implement it. This indicates that we probably + // need to introduce another class in our hierarchy. + codegen(): string { + throw new Error( + "Abstract method - must override for expr: " + + // eslint-disable-next-line @babel/no-invalid-this + this.print(), + ); } - compile() { + compile(): (vars: Expr[]) => string { var code = this.codegen(); try { + // @ts-expect-error: TypeScript doesn't want to unify + // `Function` with the `compile`'s return type. return new Function("vars", "return " + code + ";"); } catch (e) { throw new Error("Function did not compile: " + code); @@ -126,14 +189,10 @@ class Expr { // returns a string unambiguously representing the expression // should be valid as input // e.g. this.equals(parse(this.print())) === true - print() { - return abstract(); - } + abstract print(): string; // returns a TeX string representing the expression - tex() { - return abstract(); - } + abstract tex(): string; // returns a TeX string, modified by the given options asTex(options) { @@ -144,7 +203,7 @@ class Expr { times: false, }); - var tex = this.tex(); + let tex: string = this.tex(); if (options.display) { tex = "\\displaystyle " + tex; @@ -161,65 +220,59 @@ class Expr { } // returns the name of this expression's constructor as a string - // only used for testing and debugging (the ugly regex is for IE8) - name() { - if (this.func.name) { - return this.func.name; - } else { - return this.func.toString().match(/^function\s*([^\s(]+)/)[1]; - } + // only used for testing and debugging + name(): string { + return this.func.name; } // returns a string representing current node structure - repr() { + repr(): string { return ( this.name() + "(" + _.map(this.args(), function (arg) { - return _.isString(arg) ? arg : arg.repr(); + return _.isString(arg) || _.isNumber(arg) ? arg : arg.repr(); }).join(",") + ")" ); } // removes all negative signs - strip() { + strip(): Expr { return this.recurse("strip"); } // canonically reorders all commutative elements - normalize() { + normalize(): Expr { return this.recurse("normalize"); } // expands the expression - expand() { + expand(): Expr { return this.recurse("expand"); } // naively factors out like terms - factor(options) { + factor(options): Expr { return this.recurse("factor", options); } // collect all like terms - collect(options) { + collect(options?: Options): Expr { return this.recurse("collect", options); } // strict syntactic equality check - equals(other) { + equals(other: Expr): boolean { return this.normalize().print() === other.normalize().print(); } // expand and collect until the expression no longer changes - simplify(options) { - options = _.extend( - { - once: false, - }, - options, - ); + simplify(options?: Options) { + options = { + once: false, + ...options, + }; // Attempt to factor and collect var step1 = this.factor(options); @@ -231,7 +284,7 @@ class Expr { } // Attempt to expand and collect - var step3 = step2.expand(options); + var step3 = step2.expand(); var step4 = step3.collect(options); // Rollback if collect didn't do anything @@ -255,14 +308,12 @@ class Expr { } // return the child nodes of this node - exprArgs() { - return _.filter(this.args(), function (arg) { - return arg instanceof Expr; - }); + exprArgs(): Expr[] { + return this.args().filter(isExpr); } // return the variables (function and non) within the expression - getVars(excludeFunc) { + getVars(excludeFunc?: boolean) { return _.uniq( _.flatten(_.invoke(this.exprArgs(), "getVars", excludeFunc)), ).sort(); @@ -277,12 +328,12 @@ class Expr { } // check whether this expression node is of a particular type - is(func) { + is(func): boolean { return this instanceof func; } // check whether this expression has a particular node type - has(func) { + has(func): boolean { if (this instanceof func) { return true; } @@ -293,7 +344,7 @@ class Expr { // raise this expression to a given exponent // most useful for eventually implementing i^3 = -i, etc. - raiseToThe(exp) { + raiseToThe(exp: Expr, options?: {preciseFloats?: boolean}): Expr { return new Pow(this, exp); } @@ -310,12 +361,12 @@ class Expr { } // whether this node needs an explicit multiplication sign if following a Num - needsExplicitMul() { - return this.args()[0].needsExplicitMul(); + needsExplicitMul(): boolean { + return this.exprArgs()[0].needsExplicitMul(); } // check that the variables in both expressions are the same - sameVars(other) { + sameVars(other: Expr) { var vars1 = this.getVars(); var vars2 = other.getVars(); @@ -338,7 +389,7 @@ class Expr { // 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(other) { + compare(other: Expr) { // equation comparisons are handled by Eq.compare() if (other instanceof Eq) { return false; @@ -352,7 +403,7 @@ class Expr { // 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) { + var getDelta = function (num1: number, num2: number) { if (Math.abs(num1) < 1 || Math.abs(num2) < 1) { return Math.abs(num1 - num2); } else { @@ -360,7 +411,7 @@ class Expr { } }; - var equalNumbers = function (num1, num2) { + var equalNumbers = function (num1: number, num2: number) { var delta = getDelta(num1, num2); return ( num1 === num2 /* needed if either is +/- Infinity */ || @@ -424,20 +475,20 @@ class Expr { : _.random(-range, range); }); - var equal; + let equal; if ( expr1.has(Func) || expr2.has(Func) || expr1.has(Unit) || expr2.has(Unit) ) { - var result1 = expr1.partialEval(vars); - var result2 = expr2.partialEval(vars); + const result1 = expr1.partialEval(vars); + const result2 = expr2.partialEval(vars); equal = result1.simplify().equals(result2.simplify()); } else { - var result1 = expr1.eval(vars); - var result2 = expr2.eval(vars); + const result1 = expr1.eval(vars); + const result2 = expr2.eval(vars); equal = equalNumbers(result1, result2); } @@ -451,11 +502,11 @@ class Expr { } // evaluate as much of the expression as possible - partialEval(vars) { + partialEval(vars: Vars): Expr { if (this instanceof Unit) { return this; } else if (!this.has(Func)) { - return new Float(this.eval(vars).toFixed(TOLERANCE)).collect(); + return new Float(+this.eval(vars).toFixed(TOLERANCE)).collect(); } else if (this instanceof Func) { return new Func(this.symbol, this.arg.partialEval(vars)); } else { @@ -467,29 +518,33 @@ class Expr { // 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(other) { + sameForm(other: Expr) { return this.strip().equals(other.strip()); } // returns the GCD of this expression and the given factor - findGCD(factor) { - return this.equals(factor) ? factor : Num.One; + findGCD(factor: Expr): Expr { + return this.equals(factor) ? factor : NumOne; } // return this expression's denominator - getDenominator() { - return Num.One; + getDenominator(): Expr { + return NumOne; } // return this expression as a Mul asMul() { - return new Mul(Num.One, this); + return new Mul(NumOne, this); } // TODO(alex): rename to isDefinitePositive or similar? // return whether this expression is 100% positive - isPositive() { - return abstract(); + isPositive(): boolean { + throw new Error( + "Abstract method - must override for expr: " + + // eslint-disable-next-line @babel/no-invalid-this + this.print(), + ); } // TODO(alex): rename to hasNegativeSign or similar? @@ -499,12 +554,12 @@ class Expr { } // return a factor of this expression that is 100% positive - asPositiveFactor() { - return this.isPositive() ? this : Num.One; + asPositiveFactor(): Expr { + return this.isPositive() ? this : NumOne; } // return a copy of the expression with a new hint set (preserves hints) - addHint(hint) { + addHint(hint: keyof Hints) { if (!hint) { return this; } @@ -515,32 +570,32 @@ class Expr { return expr; } - // hints = { - // parens: false, - // }; - - // currently unused! - asExpr() { - return this; - } - // complete parse by performing a few necessary transformations - completeParse() { + completeParse(): Expr { return this.recurse("completeParse"); } - abs() { - return abstract(); + abs(): Expr { + throw new Error( + "Abstract method - must override for expr: " + + // eslint-disable-next-line @babel/no-invalid-this + this.print(), + ); } - negate() { - return new Mul(Num.Neg, this); + negate(): Expr { + return new Mul(NumNeg, this); } } /* abstract sequence node */ -class Seq extends Expr { - constructor(...args) { +abstract class Seq extends Expr { + // This should always have at least two terms. + // TODO(kevinb): Try enforcing this at the type-level using [T, T, T[]] + terms: Expr[]; + + // TODO(kevinb): Update this use `...args: Expr[]` + constructor(...args: any[]) { super(); if (args.length === 1) { this.terms = args[0]; @@ -554,59 +609,51 @@ class Seq extends Expr { } normalize() { - var terms = _.sortBy( - _.invoke(this.terms, "normalize"), - function (term) { - return term.print(); - }, - ); + var terms = _.sortBy(_.invoke(this.terms, "normalize"), (term) => { + return term.print(); + }); return new this.func(terms); } - expand() { + expand(): Expr { 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() { - var terms = _.groupBy(this.terms, function (term) { + partition(): [Seq, Seq] { + var [numbers, others] = partition(this.terms, (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() { + flatten(): Expr { var type = this; - var terms = _.reject(this.terms, function (term) { + var terms = _.reject(this.terms, (term) => { + // @ts-expect-error: `identity` is defined on Add and Mul but doesn't + // exist on Seq itself. return term.equals(type.identity); }); if (terms.length === 0) { + // @ts-expect-error: `identity` is defined on Add and Mul but doesn't + // exist on Seq itself. return type.identity; } if (terms.length === 1) { return terms[0]; } - var grouped = _.groupBy(terms, function (term) { + // same contains the children which are Seqs of the same type as this Seq + const [same, others] = partition(terms, (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), ); @@ -614,9 +661,7 @@ class Seq extends Expr { } // reduce a numeric sequence to a Num - reduce(options) { - return abstract(); - } + abstract reduce(options?: Options): Expr; isPositive() { var terms = _.invoke(this.terms, "collect"); @@ -626,17 +671,12 @@ class Seq extends Expr { // 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(oldTerm, newTerm) { - var index; - - if (oldTerm instanceof Expr) { - index = _.indexOf(this.terms, oldTerm); - } else { - index = oldTerm; - } + replace(oldTerm: Expr | number, newTerm?: Expr | Expr[]) { + const index = + oldTerm instanceof Expr ? _.indexOf(this.terms, oldTerm) : oldTerm; - var newTerms = []; - if (_.isArray(newTerm)) { + var newTerms: Expr[] = []; + if (Array.isArray(newTerm)) { newTerms = newTerm; } else if (newTerm) { newTerms = [newTerm]; @@ -651,11 +691,11 @@ class Seq extends Expr { } // syntactic sugar for replace() - remove(term) { + remove(term: number | Expr) { return this.replace(term); } - getDenominator() { + getDenominator(): Expr { // TODO(alex): find and return LCM return new Mul(_.invoke(this.terms, "getDenominator")).flatten(); } @@ -663,34 +703,36 @@ class Seq extends Expr { /* sequence of additive terms */ export class Add extends Seq { + identity: Num = NumZero; + func = Add; - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { return _.reduce( this.terms, - function (memo, term) { + (memo, term) => { return memo + term.eval(vars, options); }, 0, ); } - codegen() { + codegen(): string { return ( - _.map(this.terms, function (term) { + _.map(this.terms, (term) => { return "(" + term.codegen() + ")"; }).join(" + ") || "0" ); } - print() { + print(): string { return _.invoke(this.terms, "print").join("+"); } - tex() { - var tex = ""; + tex(): string { + let tex = ""; - _.each(this.terms, function (term) { + _.each(this.terms, (term) => { if (!tex || term.isSubtract()) { tex += term.tex(); } else { @@ -701,30 +743,28 @@ export class Add extends Seq { return tex; } - collect(options) { + collect(options?: Options) { var terms = _.invoke(this.terms, "collect", options); + var pairs: [expr: Expr, coefficient: Expr][] = []; - // [Expr expr, Num coefficient] - var pairs = []; - - _.each(terms, function (term) { + _.each(terms, (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]); + pairs.push([NumOne, term]); } else { - pairs.push([term, Num.One]); + pairs.push([term, NumOne]); } }); // { (Expr expr).print(): [[Expr expr, Num coefficient]] } - var grouped = _.groupBy(pairs, function (pair) { + var grouped = _.groupBy(pairs, (pair) => { return pair[0].normalize().print(); }); var collected = _.compact( - _.map(grouped, function (pairs) { + _.map(grouped, (pairs) => { var expr = pairs[0][0]; var sum = new Add(_.zip.apply(_, pairs)[1]); var coefficient = sum.reduce(options); @@ -740,16 +780,9 @@ export class Add extends Seq { // naively factor out anything that is common to all terms // if options.keepNegative is specified, won't factor out a common -1 - factor(options) { - options = _.extend( - { - keepNegative: false, - }, - options, - ); - - var terms = _.invoke(this.terms, "collect"); - var factors; + factor(options: {keepNegative: boolean} = {keepNegative: false}) { + const terms = this.terms.map((term) => term.collect()); + let factors: Expr[]; if (terms[0] instanceof Mul) { factors = terms[0].terms; @@ -757,30 +790,30 @@ export class Add extends Seq { factors = [terms[0]]; } - _.each(_.rest(this.terms), function (term) { - factors = _.map(factors, function (factor) { + _.each(_.rest(this.terms), (term) => { + factors = _.map(factors, (factor: Expr) => { return term.findGCD(factor); }); }); if (!options.keepNegative && this.isNegative()) { - factors.push(Num.Neg); + factors.push(NumNeg); } - factors = new Mul(factors).flatten().collect(); + const left = new Mul(factors).flatten().collect(); - var remainder = _.map(terms, function (term) { - return Mul.handleDivide(term, factors).simplify(); - }); - remainder = new Add(remainder).flatten(); + const remainder = terms.map((term) => + Mul.handleDivide(term, left).simplify(), + ); + const right = new Add(remainder).flatten(); - return Mul.createOrAppend(factors, remainder).flatten(); + return Mul.createOrAppend(left, right).flatten(); } - reduce(options) { + reduce(options?: Options): Expr { return _.reduce( this.terms, - function (memo, term) { + (memo, term) => { return memo.add(term, options); }, this.identity, @@ -799,32 +832,43 @@ export class Add extends Seq { negate() { return new Add(_.invoke(this.terms, "negate")); } + + // create a new sequence unless left is already one (returns a copy) + static createOrAppend(left: Expr, right: Expr) { + if (left instanceof Add) { + return new Add(left.terms.concat(right)); + } else { + return new Add(left, right); + } + } } /* sequence of multiplicative terms */ export class Mul extends Seq { + identity: Num = NumOne; + func = Mul; - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { return _.reduce( this.terms, - function (memo, term) { + (memo, term) => { return memo * term.eval(vars, options); }, 1, ); } - codegen() { + codegen(): string { return ( - _.map(this.terms, function (term) { + _.map(this.terms, (term) => { return "(" + term.codegen() + ")"; }).join(" * ") || "0" ); } - print() { - return _.map(this.terms, function (term) { + print(): string { + return _.map(this.terms, (term) => { return term instanceof Add ? "(" + term.print() + ")" : term.print(); @@ -834,7 +878,7 @@ export class Mul extends Seq { getUnits() { var tmUnits = _(this.terms) .chain() - .map(function (term) { + .map((term) => { return term.getUnits(); }) .flatten() @@ -847,10 +891,10 @@ export class Mul extends Seq { // 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() { + tex(): string { var cdot = " \\cdot "; - var terms = _.groupBy(this.terms, function (term) { + var terms = _.groupBy(this.terms, (term) => { if (term.isDivide()) { return "inverse"; } else if (term instanceof Num) { @@ -861,7 +905,7 @@ export class Mul extends Seq { }); var inverses = terms.inverse || []; - var numbers = terms.number || []; + var numbers: Num[] = (terms.number as Num[]) || []; var others = terms.other || []; var negatives = ""; @@ -874,7 +918,7 @@ export class Mul extends Seq { var isRational = numbers[i] instanceof Rational && !(numbers[i] instanceof Int); if (isRational && others.length > 0 && inverses.length > 0) { - var withThisRemoved = numbers.slice(); + var withThisRemoved: Expr[] = numbers.slice(); withThisRemoved.splice(i, 1); var newTerms = withThisRemoved.concat(inverses).concat(others); return numbers[i].tex() + new Mul(newTerms).tex(); @@ -882,14 +926,16 @@ export class Mul extends Seq { } numbers = _.compact( - _.map(numbers, function (term) { - var hasDenom = - term instanceof Rational && !(term instanceof Int); + _.map(numbers, (term) => { var shouldPushDown = !term.hints.fraction || inverses.length > 0; - if (hasDenom && shouldPushDown) { + if ( + term instanceof Rational && + !(term instanceof Int) && + shouldPushDown + ) { // e.g. 3x/4 -> 3/4*x (internally) -> 3x/4 (rendered) - inverses.push(new Pow(new Int(term.d), Num.Div)); + inverses.push(new Pow(new Int(term.d), NumDiv)); var number = new Int(term.n); number.hints = term.hints; return _.any(term.hints) ? number : null; @@ -905,7 +951,7 @@ export class Mul extends Seq { } else { var tex = ""; - _.each(numbers, function (term) { + _.each(numbers, (term) => { if (term.hints.subtract && term.hints.entered) { negatives += "-"; tex += (tex ? cdot : "") + term.abs().tex(); @@ -923,7 +969,7 @@ export class Mul extends Seq { } }); - _.each(others, function (term) { + _.each(others, (term) => { if (term.needsExplicitMul()) { // e.g. 2*2^3 -> 2(dot)2^3 tex += (tex ? cdot : "") + term.tex(); @@ -949,48 +995,41 @@ export class Mul extends Seq { } } - strip() { - var terms = _.map(this.terms, function (term) { + strip(): Expr { + var terms = _.map(this.terms, (term) => { return term instanceof Num ? term.abs() : term.strip(); }); return new Mul(terms).flatten(); } // expand numerator and denominator separately - expand() { - var isAdd = function (term) { - return term instanceof Add; - }; - - var isInverse = function (term) { + expand(): Expr { + const isInverse = function (term: Expr): term is Pow { return term instanceof Pow && term.exp.isNegative(); }; - var isInverseAdd = function (term) { + const isInverseAdd = function (term: Expr) { return isInverse(term) && isAdd(term.base); }; - var mul = this.recurse("expand").flatten(); + const mul = this.recurse("expand").flatten(); + const factors = getFactors(mul); - var hasAdd = _.any(mul.terms, isAdd); - var hasInverseAdd = _.any(mul.terms, isInverseAdd); + const hasAdd = _.any(factors, isAdd); + const hasInverseAdd = _.any(factors, isInverseAdd); if (!(hasAdd || hasInverseAdd)) { return mul; } - var terms = _.groupBy(mul.terms, isInverse); - var normals = terms[false] || []; - var inverses = terms[true] || []; + let [inverses, normals] = partition(factors, isInverse); if (hasAdd) { - var grouped = _.groupBy(normals, isAdd); - var adds = grouped[true] || []; - var others = grouped[false] || []; + const [adds, others] = partition(normals, isAdd); // loop over each additive sequence - var expanded = _.reduce( - adds, + const expanded = _.reduce( + adds as Add[], function (expanded, add) { // loop over each expanded array of terms return _.reduce( @@ -998,19 +1037,17 @@ export class Mul extends Seq { function (temp, array) { // loop over each additive sequence's terms return temp.concat( - _.map(add.terms, function (term) { - return array.concat(term); - }), + _.map(add.terms, (term) => array.concat(term)), ); }, - [], + [] as Expr[][], ); }, - [[]], + [[]] as Expr[][], ); // join each fully expanded array of factors with remaining multiplicative factors - var muls = _.map(expanded, function (array) { + const muls = _.map(expanded, function (array) { return new Mul(others.concat(array)).flatten(); }); @@ -1018,84 +1055,81 @@ export class Mul extends Seq { } if (hasInverseAdd) { - var denominator = new Mul( + const denominator = new Mul( _.invoke(inverses, "getDenominator"), ).flatten(); - inverses = [new Pow(denominator.expand(), Num.Div)]; + inverses = [new Pow(denominator.expand(), NumDiv)]; } return new Mul(normals.concat(inverses)).flatten(); } - factor(options) { + factor(options): Expr { 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) { + var [rationals, others] = partition(factored.terms, (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) { + const ratObj = _.reduce( + rationals as Rational[], + (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); - } + const rational = + ratObj.d === 1 + ? new Int(ratObj.n) + : new Rational(ratObj.n, ratObj.d); - return new Mul((grouped[false] || []).concat(rational)).flatten(); + return new Mul(others.concat(rational)).flatten(); } - collect(options) { + collect(options?: Options): Expr { 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; + return NumZero; } - var others = partitioned[1].flatten(); + const other = 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(); + if (!(other instanceof Mul)) { + return new Mul(number, other).flatten(); } - others = others.terms; + const others = other.terms; - // [Expr base, Expr exp] - var pairs = []; + var pairs: [base: Expr, exp: Expr][] = []; - _.each(others, function (term) { + _.each(others, (term) => { if (term instanceof Pow) { pairs.push([term.base, term.exp]); } else { - pairs.push([term, Num.One]); + pairs.push([term, NumOne]); } }); // {(Expr base).print(): [[Expr base, Expr exp]]} - var grouped = _.groupBy(pairs, function (pair) { + var grouped = _.groupBy(pairs, (pair) => { return pair[0].normalize().print(); }); - // [[Expr base, Expr exp]] - var summed = _.compact( - _.map(grouped, function (pairs) { + var summed: [base: Expr, exp: Expr][] = _.compact( + _.map(grouped, (pairs): [Expr, Expr] | null => { var base = pairs[0][0]; var sum = new Add(_.zip.apply(_, pairs)[1]); var exp = sum.collect(options); @@ -1109,7 +1143,7 @@ export class Mul extends Seq { ); // XXX `pairs` is shadowed four or five times in this function - var pairs = _.groupBy(summed, function (pair) { + const groupedPairs = _.groupBy(summed, (pair) => { if (pair[0] instanceof Trig && pair[0].isBasic()) { return "trig"; } else if (pair[0] instanceof Log) { @@ -1118,25 +1152,25 @@ export class Mul extends Seq { return "expr"; } }); - var trigs = pairs.trig || []; - var logs = pairs.log || []; - var exprs = pairs.expr || []; + let trigs = (groupedPairs.trig as [Trig, Expr][]) || []; + let logs = (groupedPairs.log as [Log, Expr][]) || []; + const exprs = groupedPairs.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) { + var byArg = _.groupBy(trigs, (pair) => { return pair[0].arg.normalize().print(); }); trigs = []; - _.each(byArg, function (pairs) { - var arg = pairs[0][0].arg; + _.each(byArg, (pairs) => { + const arg = pairs[0][0].arg; // {Trig.type: Expr exp} - var funcs = {sin: Num.Zero, cos: Num.Zero}; - _.each(pairs, function (pair) { + let funcs: Record = {sin: NumZero, cos: NumZero}; + _.each(pairs, (pair) => { funcs[pair[0].type] = pair[1]; }); @@ -1156,7 +1190,7 @@ export class Mul extends Seq { // 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) { + _.each(funcs, (exp, type) => { trigs.push([new Trig(type, arg), exp]); }); }); @@ -1166,13 +1200,13 @@ export class Mul extends Seq { // combine logs with the same base // {Log.base.print(): [[Log base, Expr exp]]} - var byBase = _.groupBy(logs, function (pair) { + var byBase = _.groupBy(logs, (pair) => { return pair[0].base.normalize().print(); }); logs = []; - _.each(byBase, function (pairs) { + _.each(byBase, (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) @@ -1203,9 +1237,7 @@ export class Mul extends Seq { // 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) { + var collected = _.map([...trigs, ...logs, ...exprs], (pair) => { return new Pow(pair[0], pair[1]).collect(options); }); @@ -1213,21 +1245,19 @@ export class Mul extends Seq { } isSubtract() { - return _.any(this.terms, function (term) { - return term instanceof Num && term.hints.subtract; + return _.any(this.terms, (term) => { + return term instanceof Num && Boolean(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(hint) { + factorIn(hint: keyof Hints): Expr { var partitioned = this.partition(); - var numbers = partitioned[0].terms; - var fold = - numbers.length && - _.all(numbers, function (num) { - return num.n > 0; - }); + // `partition` splits the terms into two Seqs - one containing + // only Nums and the all non-Num nodes. + var numbers = partitioned[0].terms as Num[]; + var fold = numbers.length && _.all(numbers, (num) => num.n > 0); if (fold) { // e.g. - x*2*3 -> x*-2*3 @@ -1246,7 +1276,7 @@ export class Mul extends Seq { factorOut() { var factored = false; var terms = _.compact( - _.map(this.terms, function (term, i, list) { + _.map(this.terms, (term) => { if (!factored && term instanceof Num && term.hints.divide) { factored = true; return term.n !== -1 ? term.negate() : null; @@ -1263,17 +1293,17 @@ export class Mul extends Seq { } } - reduce(options) { + reduce(options?: {preciseFloats: boolean}) { return _.reduce( this.terms, - function (memo, term) { + (memo, term) => { return memo.mul(term, options); }, this.identity, ); } - findGCD(factor) { + findGCD(factor: Expr): Expr { return new Mul(_.invoke(this.terms, "findGCD", factor)).flatten(); } @@ -1281,17 +1311,22 @@ export class Mul extends Seq { return this; } - asPositiveFactor() { + asPositiveFactor(): Expr { if (this.isPositive()) { return this; } else { - var terms = _.invoke(this.collect().terms, "asPositiveFactor"); + const terms = getFactors(this.collect()).map((factor) => + factor.asPositiveFactor(), + ); return new Mul(terms).flatten(); } } isNegative() { - return _.any(_.invoke(this.collect().terms, "isNegative")); + const terms = getFactors(this.collect()).map((factor) => + factor.isNegative(), + ); + return _.any(terms); } fold() { @@ -1299,14 +1334,14 @@ export class Mul extends Seq { } negate() { - var isNum = function (expr) { + var isNum = (expr: Expr) => { return expr instanceof Num; }; - if (_.any(this.terms, isNum)) { - var num = _.find(this.terms, isNum); + const num = _.find(this.terms, isNum); + if (num) { return this.replace(num, num.negate()); } else { - return new Mul([Num.Neg].concat(this.terms)); + return new Mul([NumNeg].concat(this.terms)); } } @@ -1314,7 +1349,7 @@ export class Mul extends Seq { // 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 - static handleNegative(expr, hint) { + static handleNegative(expr: Expr, hint?): Expr { if (expr instanceof Num && expr.n > 0) { // e.g. - 2 -> -2 var negated = expr.negate(); @@ -1333,7 +1368,7 @@ export class Mul extends Seq { } // division can create either a Rational or a Mul - static handleDivide(left, right) { + static handleDivide(left: Expr, right: Expr): Expr { // 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]); @@ -1341,10 +1376,10 @@ export class Mul extends Seq { return Mul.handleDivide(first, rest); } - var isInt = function (expr) { + var isInt = (expr: Expr | undefined): expr is Int => { return expr instanceof Int; }; - var isRational = function (expr) { + var isRational = (expr: Expr | undefined): expr is Rational => { return expr instanceof Rational; }; @@ -1353,7 +1388,7 @@ export class Mul extends Seq { 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); + var num = reversed.find(isRational); if (!isInt(num)) { return new Mul( @@ -1375,19 +1410,19 @@ export class Mul extends Seq { var result; if (num.n < 0 && right.n < 0) { rational.d = -rational.d; - return left.replace(num, [Num.Neg, rational]); + return left.replace(num, [NumNeg, rational]); } else { return left.replace(num, rational); } } - var divide = function (a, b) { + var divide = (a: Expr, b: Expr) => { 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, + NumNeg, new Rational(a.n, -b.n).addHint("fraction"), ]; } else { @@ -1423,7 +1458,7 @@ export class Mul extends Seq { pow = new Pow(b.base, Mul.handleNegative(b.exp, "divide")); } else { // e.g. x ^ -1 -> x^-1 - pow = new Pow(b, Num.Div); + pow = new Pow(b, NumDiv); } if (a instanceof Int && a.n === 1) { @@ -1437,7 +1472,9 @@ export class Mul extends Seq { }; if (left instanceof Mul) { - var divided = divide(_.last(left.terms), right); + // NOTE(kevinb): `terms` should always have at least two + // elements so getting the last element is safe to do. + var divided = divide(_.last(left.terms)!, right); return new Mul(_.initial(left.terms).concat(divided)); } else { var divided = divide(left, right); @@ -1457,19 +1494,19 @@ export class Mul extends Seq { // 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) - static fold(expr) { + static fold(expr: Expr): Expr { if (expr instanceof Mul) { // assuming that this will be second to last - var trigLog = _.find(_.initial(expr.terms), function (term) { + var trigLog = _.find(_.initial(expr.terms), (term) => { return ( (term instanceof Trig || term instanceof Log) && - term.hints.open + Boolean(term.hints.open) ); - }); - var index = _.indexOf(expr.terms, trigLog); + }) as Trig | Log | undefined; if (trigLog) { - var last = _.last(expr.terms); + // expr.terms should always have at least two terms + const last = _.last(expr.terms)!; if ( trigLog.hints.parens || last.hints.parens || @@ -1478,19 +1515,21 @@ export class Mul extends Seq { ) { 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(), - ); - } - + const newTrigLog = + trigLog instanceof Trig + ? Trig.create( + [trigLog.type, trigLog.exp], + Mul.createOrAppend(trigLog.arg, last).fold(), + ) + : Log.create( + trigLog.base, + Mul.createOrAppend( + trigLog.power, + last, + ).fold(), + ); + + const index = _.indexOf(expr.terms, trigLog); if (index === 0) { return newTrigLog; } else { @@ -1504,24 +1543,26 @@ export class Mul extends Seq { var partitioned = expr.partition(); var numbers = partitioned[0].terms; - var pos = function (num) { + var pos = (num: Num): boolean => { return num.n > 0; }; - var neg = function (num) { - return num.n === -1 && num.hints.negate; + var neg = (num: Num): boolean => { + return num.n === -1 && Boolean(num.hints.negate); }; - var posOrNeg = function (num) { + var posOrNeg = function (num: Num): boolean { return pos(num) || neg(num); }; + const posNum = numbers.find(pos); + const negNum = numbers.find(neg); if ( numbers.length > 1 && - _.some(numbers, neg) && - _.some(numbers, pos) && + negNum && + posNum && _.every(numbers, posOrNeg) ) { - var firstNeg = _.indexOf(expr.terms, _.find(expr.terms, neg)); - var firstNum = _.indexOf(expr.terms, _.find(expr.terms, pos)); + var firstNeg = _.indexOf(expr.terms, negNum); + var firstNum = _.indexOf(expr.terms, posNum); // e.g. -x*2 -> x*-2 if (firstNeg < firstNum) { @@ -1535,22 +1576,22 @@ export class Mul extends Seq { // in all other cases, make no change return expr; } -} -// static methods for the sequence types -_.each([Add, Mul], function (cls) { // create a new sequence unless left is already one (returns a copy) - cls.createOrAppend = function (left, right) { - if (left instanceof cls) { - return new cls(left.terms.concat(right)); + static createOrAppend(left: Expr, right: Expr) { + if (left instanceof Mul) { + return new Mul(left.terms.concat(right)); } else { - return new cls(left, right); + return new Mul(left, right); } - }; -}); + } +} /* exponentiation */ export class Pow extends Expr { + base: Expr; + exp: Expr; + constructor(base, exp) { super(); this.base = base; @@ -1563,7 +1604,7 @@ export class Pow extends Expr { return [this.base, this.exp]; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { var evaledBase = this.base.eval(vars, options); var evaledExp = this.exp.eval(vars, options); @@ -1582,7 +1623,7 @@ export class Pow extends Expr { // 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 decimals = (num - +num.toFixed()).toString().length - 2; var denominator = Math.pow(10, decimals); var rationalExp = new Rational(num * denominator, denominator); simplifiedExp = rationalExp.simplify(); @@ -1600,23 +1641,22 @@ export class Pow extends Expr { } getUnits() { - return this.base.getUnits().map( - function (unit) { - return { - unit: unit.unit, - pow: unit.pow * this.exp.n, - }; - }.bind(this), - ); + return this.base.getUnits().map((unit) => { + return { + unit: unit.unit, + // Exponents in units should always be integers + pow: unit.pow * (this.exp as Int).n, + }; + }); } - codegen() { + codegen(): string { return ( "Math.pow(" + this.base.codegen() + ", " + this.exp.codegen() + ")" ); } - print() { + print(): string { var base = this.base.print(); if (this.base instanceof Seq || this.base instanceof Pow) { base = "(" + base + ")"; @@ -1624,13 +1664,13 @@ export class Pow extends Expr { return base + "^(" + this.exp.print() + ")"; } - tex() { + tex(): string { if (this.isDivide()) { // e.g. x ^ -1 w/hint -> 1/x return "\\frac{1}{" + this.asDivide().tex() + "}"; - } else if (this.isRoot()) { + } else if (this.isRoot() && isRational(this.exp)) { if (this.exp.n !== 1) { - error( + throw new Error( "Node marked with hint 'root' does not have exponent " + "of form 1/x.", ); @@ -1651,7 +1691,7 @@ export class Pow extends Expr { this.exp.eval() >= 0 ) { // e.g sin(x) ^ 2 -> sin^2(x) - var split = this.base.tex({split: true}); + var split = this.base.texSplit(); return split[0] + "^{" + this.exp.tex() + "}" + split[1]; } else { // e.g. x ^ y -> x^y @@ -1681,7 +1721,7 @@ export class Pow extends Expr { if (pow.base instanceof Mul) { // e.g. (ab)^c -> a^c*b^c - var terms = _.map(pow.base.terms, function (term) { + var terms = _.map(pow.base.terms, (term) => { return new Pow(term, pow.exp); }); @@ -1698,23 +1738,23 @@ export class Pow extends Expr { var n = pow.exp.abs().eval(); var signed = function (mul) { - return positive ? mul : new Pow(mul, Num.Div); + return positive ? mul : new Pow(mul, NumDiv); }; // compute and cache powers of 2 up to n - var cache = {1: pow.base}; + const cache: Record = {1: pow.base}; for (var i = 2; i <= n; i *= 2) { - var mul = new Mul(cache[i / 2], cache[i / 2]); + const mul = new Mul(cache[i / 2], cache[i / 2]); cache[i] = mul.expand().collect(); } // if n is a power of 2, you're done! - if (_.has(cache, n)) { + if (n in cache) { return signed(cache[n]); } // otherwise decompose n into powers of 2 ... - var indices = _.map( + let indices = _.map( n.toString(2).split(""), function (str, i, list) { return Number(str) * Math.pow(2, list.length - i - 1); @@ -1723,13 +1763,19 @@ export class Pow extends Expr { indices = _.without(indices, 0); // ... then combine - var mul = new Mul(_.pick(cache, indices)).expand().collect(); + const factors: Expr[] = []; + for (const index of indices) { + if (index in cache) { + factors.push(cache[index]); + } + } + const mul = new Mul(factors).expand().collect(); return signed(mul); } else if (pow.exp instanceof Add) { // DEFINITELY want behind super-simplify() flag // e.g. x^(a+b) -> x^a*x^b - var terms = _.map(pow.exp.terms, function (term) { + const terms = _.map(pow.exp.terms, (term) => { return new Pow(pow.base, term).expand(); }); @@ -1742,8 +1788,8 @@ export class Pow extends Expr { factor() { var pow = this.recurse("factor"); if (pow.base instanceof Mul) { - var terms = _.map(pow.base.terms, function (term) { - if (term instanceof Int && pow.exp.equals(Num.Div)) { + var terms = _.map(pow.base.terms, (term) => { + if (term instanceof Int && pow.exp.equals(NumDiv)) { // Anything that can be a Rational should be a Rational // e.g. 2^(-1) -> 1/2 return new Rational(1, term.n); @@ -1757,25 +1803,25 @@ export class Pow extends Expr { } } - collect(options) { + collect(options?: Options): Expr { if (this.base instanceof Pow) { // collect this first to avoid having to deal with float precision // e.g. sqrt(2)^2 -> 2, not 2.0000000000000004 // e.g. (x^y)^z -> x^(yz) - var base = this.base.base; - var exp = Mul.createOrAppend(this.base.exp, this.exp); + const base = this.base.base; + const exp = Mul.createOrAppend(this.base.exp, this.exp); return new Pow(base, exp).collect(options); } - var pow = this.recurse("collect", options); + const pow = this.recurse("collect", options); - var isSimilarLog = function (term) { + const isSimilarLog = function (term: Expr): term is Log { return term instanceof Log && term.base.equals(pow.base); }; if (pow.exp instanceof Num && pow.exp.eval() === 0) { // e.g. x^0 -> 1 - return Num.One; + return NumOne; } else if (pow.exp instanceof Num && pow.exp.eval() === 1) { // e.g. x^1 -> x return pow.base; @@ -1787,9 +1833,11 @@ export class Pow extends Expr { _.any(pow.exp.terms, isSimilarLog) ) { // e.g. b^(2*y*log_b(x)) -> x^(2*y) - var log = _.find(pow.exp.terms, isSimilarLog); - var base = log.power; - var exp = pow.exp.remove(log).flatten(); + // `log` will always be defined here because of the + // `_.any(pow.exp.terms, isSimilarLog)` check above. + const log = pow.exp.terms.find(isSimilarLog)!; + const base = log.power; + const exp = pow.exp.remove(log).flatten(); return new Pow(base, exp).collect(options); } else if (pow.base instanceof Num && pow.exp instanceof Num) { // TODO(alex): Consider encapsualting this logic (and similar logic @@ -1804,15 +1852,20 @@ export class Pow extends Expr { // as floats, but ideally rationals should be pre-processed // e.g. (1/27)^(1/3) -> 1/3 to avoid most cases. // TODO(alex): Catch such cases and avoid converting to floats. - var exp = pow.exp.asRational(); - var decimalsInBase = pow.base.getDecimalPlaces(); - var root = new Pow(pow.base, new Rational(1, exp.d)); - var decimalsInRoot = root.collect().getDecimalPlaces(); + const exp = pow.exp.asRational(); + const decimalsInBase = pow.base.getDecimalPlaces(); + const root = new Pow(pow.base, new Rational(1, exp.d)); + const decimalsInRoot: number = root + .collect() + // @ts-expect-error: we assume that `root.collect()` returns + // a Num here but tbh I'm not sure how this code isn't causing + // an infinite loop. + .getDecimalPlaces(); if (decimalsInRoot > decimalsInBase) { // Collecting over this denominator would result in an // imprecise float, so avoid doing so. - var newBase = new Pow(pow.base, new Int(exp.n)).collect(); + const newBase = new Pow(pow.base, new Int(exp.n)).collect(); return new Pow(newBase, new Rational(1, exp.d)); } } @@ -1826,8 +1879,8 @@ export class Pow extends Expr { // checks whether this Pow represents user-entered division isDivide() { - var isDiv = function (arg) { - return arg instanceof Num && arg.hints.divide; + var isDiv = function (arg: Expr) { + return arg instanceof Num && Boolean(arg.hints.divide); }; return ( isDiv(this.exp) || @@ -1849,12 +1902,14 @@ export class Pow extends Expr { } else if (this.exp instanceof Mul) { return new Pow(this.base, this.exp.factorOut()); } else { - error("called asDivide() on an Expr that wasn't a Num or Mul"); + throw new Error( + "called asDivide() on an Expr that wasn't a Num or Mul", + ); } } - isRoot() { - return this.exp instanceof Rational && this.exp.hints.root; + isRoot(): boolean { + return this.exp instanceof Rational && Boolean(this.exp.hints.root); } isSquaredTrig() { @@ -1886,19 +1941,15 @@ export class Pow extends Expr { } else if (this.base instanceof Num) { return new Pow(this.base.getDenominator(), this.exp).collect(); } else { - return Num.One; + return NumOne; } } - findGCD(factor) { - var base, exp; - if (factor instanceof Pow) { - base = factor.base; - exp = factor.exp; - } else { - base = factor; - exp = Num.One; - } + findGCD(factor: Expr): Expr { + const [base, exp] = + factor instanceof Pow + ? [factor.base, factor.exp] + : [factor, NumOne]; // GCD is only relevant if same base if (this.base.equals(base)) { @@ -1913,7 +1964,7 @@ export class Pow extends Expr { } else if (this.exp instanceof Num || exp instanceof Num) { // one numerical exponent // e.g. GCD(x^2, x^y) -> 1 - return Num.One; + return NumOne; } var expA = this.exp.asMul().partition(); @@ -1928,7 +1979,7 @@ export class Pow extends Expr { } } - return Num.One; + return NumOne; } isPositive() { @@ -1940,7 +1991,7 @@ export class Pow extends Expr { return exp instanceof Int && exp.eval() % 2 === 0; } - asPositiveFactor() { + asPositiveFactor(): Expr { if (this.isPositive()) { return this; } else { @@ -1955,17 +2006,19 @@ export class Pow extends Expr { return new Pow(this.base, new Int(n + 1)); } } - return Num.One; + return NumOne; } } - static sqrt(arg) { - return new Pow(arg, Num.Sqrt); + static sqrt(arg: Expr) { + return new Pow(arg, NumSqrt); } // NOTE(kevinb): nthroot is used as a constructor so we need to // define it as a static property instead of a static method. - static nthroot = function (radicand, degree) { + // TODO(kevinb): update parser-generator.ts to call nthrooth + // without using `new`. + static nthroot = function (radicand: Expr, degree: Expr) { var exp = Mul.fold(Mul.handleDivide(new Int(1), degree)); // FIXME(johnsullivan): If oneOverDegree ends up being a pow object, @@ -1976,13 +2029,17 @@ export class Pow extends Expr { /* logarithm */ export class Log extends Expr { - constructor(base, power) { + base: Expr; + power: Expr; + + constructor(base: Expr, power: Expr) { super(); this.base = base; this.power = power; - this.hints = _.extend(this.hints, { + this.hints = { + ...this.hints, open: false, - }); + }; } func = Log; @@ -1991,14 +2048,14 @@ export class Log extends Expr { return [this.base, this.power]; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { return ( Math.log(this.power.eval(vars, options)) / Math.log(this.base.eval(vars, options)) ); } - codegen() { + codegen(): string { return ( "(Math.log(" + this.power.codegen() + @@ -2008,7 +2065,7 @@ export class Log extends Expr { ); } - print() { + print(): string { var power = "(" + this.power.print() + ")"; if (this.isNatural()) { return "ln" + power; @@ -2017,7 +2074,7 @@ export class Log extends Expr { } } - tex() { + tex(): string { var power = "(" + this.power.tex() + ")"; if (this.isNatural()) { return "\\ln" + power; @@ -2026,15 +2083,15 @@ export class Log extends Expr { } } - collect(options) { + collect(options?: Options): Expr { var log = this.recurse("collect", options); if (log.power instanceof Num && log.power.eval() === 1) { // e.g. ln(1) -> 0 - return Num.Zero; + return NumZero; } else if (log.base.equals(log.power)) { // e.g. log_b(b) -> 1 - return Num.One; + return NumOne; } else if ( log.power instanceof Pow && log.power.base.equals(log.base) @@ -2053,7 +2110,7 @@ export class Log extends Expr { // might want behind super-simplify() flag // e.g. ln(xy) -> ln(x) + ln(y) - var terms = _.map(log.power.terms, function (term) { + var terms = _.map(log.power.terms, (term) => { // need to expand again in case new log powers are Pows return new Log(log.base, term).expand(); }); @@ -2081,7 +2138,11 @@ export class Log extends Expr { isPositive() { var log = this.collect(); - if (log.base instanceof Num && log.power instanceof Num) { + if ( + log instanceof Log && + log.base instanceof Num && + log.power instanceof Num + ) { return this.eval() > 0; } else { return false; @@ -2099,8 +2160,9 @@ export class Log extends Expr { static natural() { return Const.e; } + static common() { - return Num.Ten; + return NumTen; } static create(base, power) { @@ -2112,15 +2174,27 @@ export class Log extends Expr { } } +type TrigFunc = { + eval: (arg: number) => number; + codegen: string | ((argStr: string) => string); + tex: string; + expand?: () => Expr; +}; + /* trigonometric functions */ export class Trig extends Expr { - constructor(type, arg) { + type: string; // TODO(kevinb): Use a union type for this + arg: Expr; + exp?: Expr; + + constructor(type: string, arg: Expr) { super(); this.type = type; this.arg = arg; - this.hints = _.extend(this.hints, { + this.hints = { + ...this.hints, open: false, - }); + }; } func = Trig; @@ -2129,60 +2203,51 @@ export class Trig extends Expr { return [this.type, this.arg]; } - functions = { + // TODO(kevinb): Use union type for the function names. + functions: Record = { sin: { eval: Math.sin, codegen: "Math.sin((", tex: "\\sin", - expand: function () { - return this; - }, + expand: () => this, }, cos: { eval: Math.cos, codegen: "Math.cos((", tex: "\\cos", - expand: function () { - return this; - }, + expand: () => this, }, tan: { eval: Math.tan, codegen: "Math.tan((", tex: "\\tan", - expand: function () { - return Mul.handleDivide(Trig.sin(this.arg), Trig.cos(this.arg)); - }, + expand: () => + Mul.handleDivide(Trig.sin(this.arg), Trig.cos(this.arg)), }, csc: { - eval: function (arg) { + eval: (arg: number) => { return 1 / Math.sin(arg); }, codegen: "(1/Math.sin(", tex: "\\csc", - expand: function () { - return Mul.handleDivide(Num.One, Trig.sin(this.arg)); - }, + expand: () => Mul.handleDivide(NumOne, Trig.sin(this.arg)), }, sec: { - eval: function (arg) { + eval: (arg: number) => { return 1 / Math.cos(arg); }, codegen: "(1/Math.cos(", tex: "\\sec", - expand: function () { - return Mul.handleDivide(Num.One, Trig.cos(this.arg)); - }, + expand: () => Mul.handleDivide(NumOne, Trig.cos(this.arg)), }, cot: { - eval: function (arg) { + eval: (arg: number) => { return 1 / Math.tan(arg); }, codegen: "(1/Math.tan(", tex: "\\cot", - expand: function () { - return Mul.handleDivide(Trig.cos(this.arg), Trig.sin(this.arg)); - }, + expand: () => + Mul.handleDivide(Trig.cos(this.arg), Trig.sin(this.arg)), }, arcsin: { eval: Math.asin, @@ -2200,31 +2265,31 @@ export class Trig extends Expr { tex: "\\arctan", }, arccsc: { - eval: function (arg) { + eval: (arg: number) => { return Math.asin(1 / arg); }, codegen: "Math.asin(1/(", tex: "\\operatorname{arccsc}", }, arcsec: { - eval: function (arg) { + eval: (arg: number) => { return Math.acos(1 / arg); }, codegen: "Math.acos(1/(", tex: "\\operatorname{arcsec}", }, arccot: { - eval: function (arg) { + eval: (arg: number) => { return Math.atan(1 / arg); }, codegen: "Math.atan(1/(", tex: "\\operatorname{arccot}", }, sinh: { - eval: function (arg) { + eval: (arg: number) => { return (Math.exp(arg) - Math.exp(-arg)) / 2; }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "((Math.exp(" + argStr + @@ -2234,15 +2299,13 @@ export class Trig extends Expr { ); }, tex: "\\sinh", - expand: function () { - return this; - }, + expand: () => this, }, cosh: { - eval: function (arg) { + eval: (arg: number) => { return (Math.exp(arg) + Math.exp(-arg)) / 2; }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "((Math.exp(" + argStr + @@ -2252,18 +2315,16 @@ export class Trig extends Expr { ); }, tex: "\\cosh", - expand: function () { - return this; - }, + expand: () => this, }, tanh: { - eval: function (arg) { + eval: (arg: number) => { return ( (Math.exp(arg) - Math.exp(-arg)) / (Math.exp(arg) + Math.exp(-arg)) ); }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "(" + "(Math.exp(" + @@ -2281,18 +2342,14 @@ export class Trig extends Expr { ); }, tex: "\\tanh", - expand: function () { - return Mul.handleDivide( - Trig.sinh(this.arg), - Trig.cosh(this.arg), - ); - }, + expand: () => + Mul.handleDivide(Trig.sinh(this.arg), Trig.cosh(this.arg)), }, csch: { - eval: function (arg) { + eval: (arg: number) => { return 2 / (Math.exp(arg) - Math.exp(-arg)); }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "(2 / (Math.exp(" + argStr + @@ -2302,15 +2359,13 @@ export class Trig extends Expr { ); }, tex: "\\csch", - expand: function () { - return Mul.handleDivide(Num.One, Trig.sinh(this.arg)); - }, + expand: () => Mul.handleDivide(NumOne, Trig.sinh(this.arg)), }, sech: { - eval: function (arg) { + eval: (arg: number) => { return 2 / (Math.exp(arg) + Math.exp(-arg)); }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "(2 / (Math.exp(" + argStr + @@ -2320,18 +2375,16 @@ export class Trig extends Expr { ); }, tex: "\\sech", - expand: function () { - return Mul.handleDivide(Num.One, Trig.cosh(this.arg)); - }, + expand: () => Mul.handleDivide(NumOne, Trig.cosh(this.arg)), }, coth: { - eval: function (arg) { + eval: (arg: number) => { return ( (Math.exp(arg) + Math.exp(-arg)) / (Math.exp(arg) - Math.exp(-arg)) ); }, - codegen: function (argStr) { + codegen: (argStr: string) => { return ( "(" + "(Math.exp(" + @@ -2349,12 +2402,8 @@ export class Trig extends Expr { ); }, tex: "\\coth", - expand: function () { - return Mul.handleDivide( - Trig.cosh(this.arg), - Trig.sinh(this.arg), - ); - }, + expand: () => + Mul.handleDivide(Trig.cosh(this.arg), Trig.sinh(this.arg)), }, }; @@ -2370,13 +2419,13 @@ export class Trig extends Expr { return _.contains(["sin", "cos"], this.type); } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { var func = this.functions[this.type].eval; var arg = this.arg.eval(vars, options); return func(arg); } - codegen() { + codegen(): string { var func = this.functions[this.type].codegen; if (typeof func === "function") { return func(this.arg.codegen()); @@ -2387,27 +2436,33 @@ export class Trig extends Expr { } } - print() { + print(): string { return this.type + "(" + this.arg.print() + ")"; } - tex(options) { + tex(): string { var func = this.functions[this.type].tex; var arg = "(" + this.arg.tex() + ")"; - return options && options.split ? [func, arg] : func + arg; + return func + arg; + } + + texSplit(): [func: string, arg: string] { + var func = this.functions[this.type].tex; + var arg = "(" + this.arg.tex() + ")"; + return [func, arg]; } isPositive() { var trig = this.collect(); - if (trig.arg instanceof Num) { + if (trig instanceof Trig && trig.arg instanceof Num) { return this.eval() > 0; } else { return false; } } - completeParse() { + completeParse(): Expr { if (this.exp) { var pow = new Pow(this, this.exp); this.exp = undefined; @@ -2426,29 +2481,28 @@ export class Trig extends Expr { var trig = this.recurse("expand"); if (!trig.isInverse()) { // e.g. tan(x) -> sin(x)/cos(x) - var expand = trig.functions[trig.type].expand; + // NOTE(kevinb): All non-inverse trig functions have an expand property. + var expand = trig.functions[trig.type].expand!; return _.bind(expand, trig)(); } else { return trig; } } - collect(options) { + collect(options?: Options): Expr { var trig = this.recurse("collect", options); if (!trig.isInverse() && trig.arg.isNegative()) { - var arg; - if (trig.arg instanceof Num) { - arg = trig.arg.abs(); - } else { - arg = Mul.handleDivide(trig.arg, Num.Neg).collect(options); - } + const arg = + trig.arg instanceof Num + ? trig.arg.abs() + : Mul.handleDivide(trig.arg, NumNeg).collect(options); if (trig.isEven()) { // e.g. cos(-x) -> cos(x) return new Trig(trig.type, arg); } else { // e.g. sin(-x) -> -sin(x) - return new Mul(Num.Neg, new Trig(trig.type, arg)); + return new Mul(NumNeg, new Trig(trig.type, arg)); } } else { return trig; @@ -2459,7 +2513,7 @@ export class Trig extends Expr { var type = pair[0]; var exp = pair[1]; - if (exp && exp.equals(Num.Neg)) { + if (exp && exp.equals(NumNeg)) { // e.g. sin^-1(x) -> arcsin(x) type = "arc" + type; exp = undefined; @@ -2477,25 +2531,27 @@ export class Trig extends Expr { return trig; } - static sin(arg) { + static sin(arg: Expr) { return new Trig("sin", arg); } - static cos(arg) { + static cos(arg: Expr) { return new Trig("cos", arg); } - static sinh(arg) { + static sinh(arg: Expr) { return new Trig("sinh", arg); } - static cosh(arg) { + static cosh(arg: Expr) { return new Trig("cosh", arg); } } export class Abs extends Expr { - constructor(arg) { + arg: Expr; + + constructor(arg: Expr) { super(); this.arg = arg; } @@ -2506,23 +2562,23 @@ export class Abs extends Expr { return [this.arg]; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { return Math.abs(this.arg.eval(vars, options)); } - codegen() { + codegen(): string { return "Math.abs(" + this.arg.codegen() + ")"; } - print() { + print(): string { return "abs(" + this.arg.print() + ")"; } - tex() { + tex(): string { return "\\left|" + this.arg.tex() + "\\right|"; } - collect(options) { + collect(options): Expr { var abs = this.recurse("collect", options); if (abs.arg.isPositive()) { @@ -2533,7 +2589,7 @@ export class Abs extends Expr { return abs.arg.abs(); } else if (abs.arg instanceof Mul) { // e.g. |-2*pi*x| -> 2*pi*|x| - var terms = _.groupBy(abs.arg.terms, function (term) { + var terms = _.groupBy(abs.arg.terms, (term) => { if (term.isPositive()) { return "positive"; } else if (term instanceof Num) { @@ -2558,12 +2614,12 @@ export class Abs extends Expr { } // this should definitely be behind a super-simplify flag - expand() { + expand(): Expr { var abs = this.recurse("expand"); if (abs.arg instanceof Mul) { // e.g. |xyz| -> |x|*|y|*|z| - var terms = _.map(abs.arg.terms, function (term) { + var terms = _.map(abs.arg.terms, (term) => { return new Abs(term); }); return new Mul(terms); @@ -2579,7 +2635,11 @@ export class Abs extends Expr { /* equation */ export class Eq extends Expr { - constructor(left, type, right) { + left: Expr; + type: string; // TODO(kevinb): use an enum for this + right: Expr; + + constructor(left: Expr, type: string, right: Expr) { super(); this.left = left; this.type = type; @@ -2596,7 +2656,7 @@ export class Eq extends Expr { return false; } - print() { + print(): string { return this.left.print() + this.type + this.right.print(); } @@ -2609,7 +2669,7 @@ export class Eq extends Expr { ">=": " \\ge ", }; - tex() { + tex(): string { return this.left.tex() + this.signs[this.type] + this.right.tex(); } @@ -2628,13 +2688,13 @@ export class Eq extends Expr { // the expression is normalized to a canonical form // e.g. y/2=x/4 -> y/2-x/4(=0) -> 2y-x(=0) // unless unfactored is specified, will then divide through - asExpr(unfactored) { - var isZero = function (expr) { + asExpr(unfactored: boolean = false): Expr { + var isZero = (expr: Expr) => { return expr instanceof Num && expr.isSimple() && expr.eval() === 0; }; // first convert to a sequence of additive terms - var terms = []; + let terms: Expr[] = []; if (this.left instanceof Add) { terms = _.clone(this.left.terms); @@ -2666,8 +2726,8 @@ export class Eq extends Expr { denominator = denominator.asPositiveFactor(); } - if (!denominator.equals(Num.One)) { - terms = _.map(terms, function (term) { + if (!denominator.equals(NumOne)) { + terms = _.map(terms, (term) => { return Mul.createOrAppend(term, denominator).simplify({ once: true, preciseFloats: true, @@ -2684,31 +2744,22 @@ export class Eq extends Expr { // e.g. 2y-4x(=0) -> y-2x(=0) // TODO(alex): Make it an option to only divide by variables/expressions // guaranteed to be nonzero - divideThrough(expr) { - var isInequality = !this.isEquality(); + divideThrough(expr: Expr) { + const isInequality = !this.isEquality(); - var simplified = expr.simplify({once: true}); - var factored = simplified.factor({keepNegative: isInequality}); + const simplified = expr.simplify({once: true}); + const factored = simplified.factor({keepNegative: isInequality}); if (!(factored instanceof Mul)) { return expr; } - var terms = factored.terms; + const terms = factored.terms; - var isAdd = function (term) { - return term instanceof Add; - }; - var hasVar = function (term) { - return !!term.getVars().length; - }; - var isOne = function (term) { - return term.equals(Num.One); - }; + const hasVar = (term: Expr) => !!term.getVars().length; + const isOne = (term: Expr) => term.equals(NumOne); - var grouped = _.groupBy(terms, isAdd); - var adds = grouped[true] || []; - var others = grouped[false] || []; + const [adds, others] = partition(terms, isAdd); if (adds.length && this.isEquality()) { // keep only Adds @@ -2716,7 +2767,7 @@ export class Eq extends Expr { return new Mul(adds).flatten(); } - var denominator = others; + let denominator = others; if (!adds.length) { // if no Adds, keep all variable terms to preserve meaning @@ -2733,11 +2784,11 @@ export class Eq extends Expr { // don't need to divide by one denominator = _.reject(denominator, isOne); - denominator = _.map(denominator, function (term) { - return new Pow(term, Num.Div); + denominator = _.map(denominator, (term) => { + return new Pow(term, NumDiv); }); - var dividedResult = new Mul(terms.concat(denominator)).collect(); + const dividedResult = new Mul(terms.concat(denominator)).collect(); // If the end result is the same as the original factoring, // rollback the factoring and discard all intermediate steps. @@ -2752,7 +2803,7 @@ export class Eq extends Expr { return _.contains(["=", "<>"], this.type); } - compare(other) { + compare(other: Eq) { // expression comparisons are handled by Expr.compare() if (!(other instanceof Eq)) { return false; @@ -2817,40 +2868,38 @@ export class Eq extends Expr { // Assumptions: Expression is of the form a+bx, and we solve for x solveLinearEquationForVariable(variable) { var expr = this.asExpr(); - if (!expr.is(Add) || expr.terms.length !== 2) { + if (!(expr instanceof Add) || expr.terms.length !== 2) { throw new Error( "Can only handle linear equations of the form " + "a + bx (= 0)", ); } - var hasVar = function (term) { + var hasVar = (term: Expr) => { return term.has(Var) && _.contains(term.getVars(), variable.symbol); }; - var a, b; - - if (hasVar(expr.terms[0])) { - a = Mul.handleNegative(expr.terms[1]); - b = Mul.handleDivide(expr.terms[0], variable); - } else { - a = Mul.handleNegative(expr.terms[0]); - b = Mul.handleDivide(expr.terms[1], variable); - } + const termHasVar = hasVar(expr.terms[0]); + const a = termHasVar + ? Mul.handleNegative(expr.terms[1]) + : Mul.handleNegative(expr.terms[0]); + const b = termHasVar + ? Mul.handleDivide(expr.terms[0], variable) + : Mul.handleDivide(expr.terms[1], variable); return Mul.handleDivide(a, b).simplify(); } } /* abstract symbol node */ -class Symbol extends Expr { +abstract class Sym extends Expr { needsExplicitMul() { return false; } - findGCD(factor) { - if (factor instanceof Symbol || factor instanceof Num) { - return this.equals(factor) ? this : Num.One; + findGCD(factor: Expr): Expr { + if (factor instanceof Sym || factor instanceof Num) { + return this.equals(factor) ? this : NumOne; } else { return factor.findGCD(this); } @@ -2858,8 +2907,11 @@ class Symbol extends Expr { } /* function variable */ -export class Func extends Symbol { - constructor(symbol, arg) { +export class Func extends Sym { + symbol: string; + arg: Expr; + + constructor(symbol: string, arg: Expr) { super(); this.symbol = symbol; this.arg = arg; @@ -2871,15 +2923,15 @@ export class Func extends Symbol { return [this.symbol, this.arg]; } - print() { + print(): string { return this.symbol + "(" + this.arg.print() + ")"; } - tex() { + tex(): string { return this.symbol + "(" + this.arg.tex() + ")"; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { var arg = this.arg; var func = vars[this.symbol]; var newVars = _.extend(_.clone(vars), { @@ -2893,7 +2945,7 @@ export class Func extends Symbol { return parsedFunc; } - codegen() { + codegen(): string { return 'vars["' + this.symbol + '"](' + this.arg.codegen() + ")"; } @@ -2901,7 +2953,7 @@ export class Func extends Symbol { return this.arg.getUnits(); } - getVars(excludeFunc) { + getVars(excludeFunc: boolean) { if (excludeFunc) { return this.arg.getVars(); } else { @@ -2915,8 +2967,11 @@ export class Func extends Symbol { } /* variable */ -export class Var extends Symbol { - constructor(symbol, subscript) { +export class Var extends Sym { + symbol: string; + subscript: Expr; + + constructor(symbol: string, subscript: Expr) { super(); this.symbol = symbol; this.subscript = subscript; @@ -2936,7 +2991,7 @@ export class Var extends Symbol { return this; } - print() { + print(): string { var sub = ""; if (this.subscript) { sub = "_(" + this.subscript.print() + ")"; @@ -2948,14 +3003,14 @@ export class Var extends Symbol { // subscripts that consist of a single number or symbol e.g. x_a or x_42 prettyPrint() { var sub = this.subscript; - if (sub && (sub instanceof Num || sub instanceof Symbol)) { + if (sub && (sub instanceof Num || sub instanceof Sym)) { return this.symbol + "_" + sub.print(); } else { return this.print(); } } - tex() { + tex(): string { var sub = ""; if (this.subscript) { sub = "_{" + this.subscript.tex() + "}"; @@ -2968,11 +3023,15 @@ export class Var extends Symbol { return "Var(" + this.print() + ")"; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions): number { + // @ts-expect-error: values is Vars are strings, but here + // we expect them to be numbers. We should probably store + // Var and Func entries separately in the Vars type so that + // they can be typed correctly. return vars[this.prettyPrint()]; } - codegen() { + codegen(): string { return 'vars["' + this.prettyPrint() + '"]'; } @@ -2986,8 +3045,10 @@ export class Var extends Symbol { } /* constant */ -export class Const extends Symbol { - constructor(symbol) { +export class Const extends Sym { + symbol: string; + + constructor(symbol: string) { super(); this.symbol = symbol; } @@ -3002,31 +3063,40 @@ export class Const extends Symbol { return this; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions): number { if (this.symbol === "pi") { return Math.PI; } else if (this.symbol === "e") { return Math.E; + } else { + // @ts-expect-error: should we throw an error here? + return undefined; } } - codegen() { + codegen(): string { if (this.symbol === "pi") { return "Math.PI"; } else if (this.symbol === "e") { return "Math.E"; + } else { + // @ts-expect-error: should we throw an error here? + return undefined; } } - print() { + print(): string { return this.symbol; } - tex() { + tex(): string { if (this.symbol === "pi") { return "\\pi "; } else if (this.symbol === "e") { return "e"; + } else { + // @ts-expect-error: should we return this.symbol here? + return undefined; } } @@ -3051,18 +3121,21 @@ export class Const extends Symbol { } /* abstract number node */ -class Num extends Expr { +abstract class Num extends Expr { + n: number = 0; + constructor() { super(); // hints for interpreting and rendering user input - this.hints = _.extend(this.hints, { + this.hints = { + ...this.hints, negate: false, subtract: false, divide: false, root: false, fraction: false, entered: false, - }); + }; } repr() { @@ -3077,58 +3150,46 @@ class Num extends Expr { return this; } - codegen() { + codegen(): string { return this.print(); } // takes another Num and returns a new Num - add() { - return abstract(); - } + abstract add(other: Expr, options?: Options): Expr; - mul() { - return abstract(); - } + abstract mul(other: Expr, options?: Options): Expr; // returns this Num's additive inverse - negate() { - return abstract(); - } + abstract negate(): Expr; - isSubtract() { - return this.hints.subtract; + isSubtract(): boolean { + return Boolean(this.hints.subtract); } // return the absolute value of the number - abs() { - return abstract(); - } + abstract abs(): Num; - needsExplicitMul() { + needsExplicitMul(): boolean { return true; } - findGCD() { - return abstract(); - } + abstract findGCD(factor: Expr): Expr; - isPositive() { + isPositive(): boolean { return this.eval() > 0; } - isNegative() { + isNegative(): boolean { return this.eval() < 0; } - asPositiveFactor() { + asPositiveFactor(): Num { return this.isPositive() ? this : this.abs(); } // whether a number is considered simple (one term) // e.g. for reals, ints and floats are simple - isSimple() { - return abstract(); - } + abstract isSimple(): boolean; // Based on http://stackoverflow.com/a/10454560/2571482 getDecimalPlaces() { @@ -3146,17 +3207,15 @@ class Num extends Expr { } } - asRational() { - return abstract(); - } + abstract asRational(): Rational; static negativeOne(hint) { if (hint === "subtract") { - return Num.Sub; + return NumSub; } else if (hint === "divide") { - return Num.Div; + return NumDiv; } else { - return Num.Neg; + return NumNeg; } } @@ -3183,22 +3242,21 @@ class Num extends Expr { return a; } - static min(...args) { - return _.min(args, function (num) { - return num.eval(); - }); + static min(...args: Expr[]) { + return _.min(args, (num) => num.eval()); } - static max(...args) { - return _.max(args, function (num) { - return num.eval(); - }); + static max(...args: Expr[]) { + return _.max(args, (num) => num.eval()); } } /* rational number (n: numerator, d: denominator) */ export class Rational extends Num { - constructor(numerator, denominator) { + n: number; + d: number; + + constructor(numerator: number, denominator: number) { super(); var n = numerator; var d = denominator; @@ -3220,11 +3278,11 @@ export class Rational extends Num { return this.n / this.d; } - print() { + print(): string { return this.n.toString() + "/" + this.d.toString(); } - tex() { + tex(): string { var tex = "\\frac{" + Math.abs(this.n).toString() + @@ -3234,7 +3292,7 @@ export class Rational extends Num { return this.n < 0 ? "-" + tex : tex; } - add(num, options) { + add(num: Num, options?: {preciseFloats: boolean}) { if (num instanceof Rational) { return new Rational( this.n * num.d + this.d * num.n, @@ -3245,7 +3303,7 @@ export class Rational extends Num { } } - mul(num, options) { + mul(num: Num, options?: {preciseFloats: boolean}) { if (num instanceof Rational) { return new Rational(this.n * num.n, this.d * num.d).collect(); } else { @@ -3274,7 +3332,7 @@ export class Rational extends Num { return new Rational(Math.abs(this.n), this.d); } - findGCD(factor) { + findGCD(factor: Expr): Expr { // Attempt to factor out common numerators and denominators to return // a Rational instead of a Float if (factor instanceof Rational) { @@ -3292,7 +3350,7 @@ export class Rational extends Num { } // for now, assuming that exp is a Num - raiseToThe(exp) { + raiseToThe(exp: Expr) { if (exp instanceof Int) { var positive = exp.eval() > 0; var abs = exp.abs().eval(); @@ -3312,18 +3370,18 @@ export class Rational extends Num { return new Int(this.d); } - isSimple() { + isSimple(): boolean { return false; } - asRational() { + asRational(): Rational { return this; } } /* integer (n: numerator/number) */ export class Int extends Rational { - constructor(number) { + constructor(number: number) { super(number, 1); } @@ -3333,11 +3391,11 @@ export class Int extends Rational { return [this.n]; } - print() { + print(): string { return this.n.toString(); } - tex() { + tex(): string { return this.n.toString(); } @@ -3353,7 +3411,7 @@ export class Int extends Rational { return true; } - findGCD(factor) { + findGCD(factor: Expr): Expr { if (factor instanceof Int) { return new Int(Num.findGCD(this.n, factor.n)); } else { @@ -3361,14 +3419,16 @@ export class Int extends Rational { } } - static create(n) { + static create(n: number) { return new Int(n).addHint("entered"); } } /* float (n: number) */ export class Float extends Num { - constructor(number) { + n: number; + + constructor(number: number) { super(); this.n = number; } @@ -3385,15 +3445,15 @@ export class Float extends Num { // TODO(alex): when we internationalize number parsing/display // we should make sure to use the appropriate decimal mark here - print() { + print(): string { return this.n.toString(); } - tex() { + tex(): string { return this.n.toString(); } - add(num, options) { + add(num: Num, options?: {preciseFloats: boolean}): Num { if (options && options.preciseFloats) { return Float.toDecimalPlaces( this.n + num.eval(), @@ -3404,7 +3464,7 @@ export class Float extends Num { } } - mul(num, options) { + mul(num: Num, options?: {preciseFloats: boolean}): Num { if (options && options.preciseFloats) { return Float.toDecimalPlaces( this.n * num.eval(), @@ -3415,13 +3475,13 @@ export class Float extends Num { } } - collect() { + collect(options?: Options): Float { // We used to simplify Floats to Ints here whenever possible, but no // longer do so in order to preserve significant figures. return this; } - negate() { + negate(): Float { return new Float(-this.n); } @@ -3429,7 +3489,7 @@ export class Float extends Num { return new Float(Math.abs(this.n)); } - findGCD(factor) { + findGCD(factor: Expr): Expr { if (factor instanceof Num) { return new Float(Num.findGCD(this.eval(), factor.eval())).collect(); } else { @@ -3438,7 +3498,7 @@ export class Float extends Num { } // for now, assuming that exp is a Num - raiseToThe(exp, options) { + raiseToThe(exp: Expr, options?: {preciseFloats?: boolean}) { if ( options && options.preciseFloats && @@ -3455,7 +3515,7 @@ export class Float extends Num { } // only to be used on non-repeating decimals (e.g. user-provided) - asRational() { + asRational(): Rational { var parts = this.n.toString().split("."); if (parts.length === 1) { return new Rational(this.n, 1); @@ -3485,21 +3545,17 @@ export class Float extends Num { } } -Num.Neg = new Int(-1).addHint("negate"); -Num.Sub = new Int(-1).addHint("subtract"); -Num.Div = new Int(-1).addHint("divide"); - -Num.Sqrt = new Rational(1, 2).addHint("root"); +const NumNeg = new Int(-1).addHint("negate"); +const NumSub = new Int(-1).addHint("subtract"); +const NumDiv = new Int(-1).addHint("divide"); -Num.Zero = new Int(0); -Num.One = new Int(1); -Num.Ten = new Int(10); +const NumSqrt = new Rational(1, 2).addHint("root"); -// set identities here -Add.prototype.identity = Num.Zero; -Mul.prototype.identity = Num.One; +const NumZero = new Int(0); +const NumOne = new Int(1); +const NumTen = new Int(10); -var parseError = function (str, hash) { +var parseError = function (str: string, hash) { // return int location of parsing error throw new Error(hash.loc.first_column); }; @@ -3533,7 +3589,12 @@ parser.yy = { }, }; -export const parse = function (input, options) { +type ParseOptions = { + functions?: string[]; + decimal_separator?: string; +}; + +export const parse = function (input: string, options?: ParseOptions) { try { if (options && options.functions) { // reserve the symbol "i" for complex numbers @@ -3553,13 +3614,15 @@ export const parse = function (input, options) { var expr = parser.parse(input).completeParse(); return {parsed: true, expr: expr}; } catch (e) { - return {parsed: false, error: e.message}; + return {parsed: false, error: (e as Error).message}; } }; /* unit */ -export class Unit extends Symbol { - constructor(symbol) { +export class Unit extends Sym { + symbol: string; + + constructor(symbol: string) { super(); this.symbol = symbol; } @@ -3574,7 +3637,7 @@ export class Unit extends Symbol { return this; } - eval(vars, options) { + eval(vars: Vars = {}, options?: ParseOptions) { // This is called when comparing units. A unit doesn't affect the // numerical value of its coefficient, so this needs to be 1. // @@ -3589,20 +3652,20 @@ export class Unit extends Symbol { return [{unit: this.symbol, pow: 1}]; } - codegen() { + codegen(): string { return "1"; } - print() { + print(): string { return this.symbol; } - tex() { + tex(): string { return this.symbol; } // Simplify units by replacing prefixes with multiplication - collect(options) { + collect(options?: Options) { if (_(baseUnits).has(this.symbol)) { return this; } else if (_(derivedUnits).has(this.symbol)) { @@ -3617,13 +3680,13 @@ export class Unit extends Symbol { // // "g" -> Unit("g") // "kg" -> 1000 * Unit("g") -var unprefixify = function (symbol) { +var unprefixify = function (symbol: string) { if (_(baseUnits).has(symbol) || _(derivedUnits).has(symbol)) { return new Unit(symbol); } // check for prefix - var prefix = _(_(siPrefixes).keys()).find(function (testPrefix) { + var prefix = _(_(siPrefixes).keys()).find((testPrefix) => { return new RegExp("^" + testPrefix).test(symbol); }); @@ -3649,7 +3712,7 @@ var unprefixify = function (symbol) { } }; -export const unitParse = function (input) { +export const unitParse = function (input: string) { try { var parseResult = unitParser.parse(input); @@ -3668,15 +3731,15 @@ export const unitParse = function (input) { // // denom is optionally null - var unitArray = []; + const unitArray: Pow[] = []; - _(parseResult.unit.num).each(function (unitSpec) { + _(parseResult.unit.num).each((unitSpec) => { unitArray.push( new Pow(unprefixify(unitSpec.name), new Int(unitSpec.pow)), ); }); - _(parseResult.unit.denom).each(function (unitSpec) { + _(parseResult.unit.denom).each((unitSpec) => { unitArray.push( new Pow(unprefixify(unitSpec.name), new Int(-1 * unitSpec.pow)), ); @@ -3687,9 +3750,10 @@ export const unitParse = function (input) { if (parseResult.type === "unitMagnitude") { // in the first case we have a magnitude coefficient as well as the // unit itself. - var coefArray = [new Float(+parseResult.magnitude)].concat( - unitArray, - ); + var coefArray: Expr[] = [ + new Float(+parseResult.magnitude), + ...unitArray, + ]; var expr = new Mul(coefArray); return { parsed: true, @@ -3707,7 +3771,7 @@ export const unitParse = function (input) { }; } } catch (e) { - return {parsed: false, error: e.message}; + return {parsed: false, error: (e as Error).message}; } }; @@ -3744,42 +3808,36 @@ var siPrefixes = { }; // Use these two values to mark a unit as either SI-prefixable or not. -var hasPrefixes = {}; -var hasntPrefixes = {}; +const hasPrefixes = "hasPrefixes"; +const hasntPrefixes = "hasntPrefixes"; + +type Prefixes = typeof hasPrefixes | typeof hasntPrefixes; -var makeAlias = function (str, prefixes) { +const makeAlias = function (str: string, prefixes: Prefixes) { var splits = str.split("|"); var coefficientStr = splits[0].trim(); var unitsStr = splits[1].trim(); - var coefficient = Num.One; + var coefficient = NumOne; if (coefficientStr !== "") { coefficient = parse(coefficientStr).expr; } var numdenomStr = unitsStr.split("/"); - var numdenom = [coefficient]; + var numdenom: Expr[] = [coefficient]; if (numdenomStr[0]) { numdenomStr[0] .split(" ") - .filter(function (x) { - return x !== ""; - }) - .map(function (x) { - numdenom.push(new Unit(x)); - }); + .filter((x) => x !== "") + .forEach((x) => numdenom.push(new Unit(x))); } if (numdenomStr[1]) { numdenomStr[1] .split(" ") - .filter(function (x) { - return x !== ""; - }) - .map(function (x) { - numdenom.push(new Pow(new Unit(x), Num.Div)); - }); + .filter((x) => x !== "") + .forEach((x) => numdenom.push(new Pow(new Unit(x), NumDiv))); } return { @@ -3954,5 +4012,5 @@ var derivedUnits = { Hz: makeAlias("| / s", hasPrefixes), }; -export const Zero = Num.Zero; -export const One = Num.One; +export const Zero = NumZero; +export const One = NumOne;