From 93bd39b6b3d7751ef269bd2ac8b7380c52886921 Mon Sep 17 00:00:00 2001 From: kevinb-khan Date: Thu, 17 Oct 2024 11:20:50 -0400 Subject: [PATCH] Convert nodes.js to TypeScript (#1748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: After updating nodes.js to ues ES6 classes in #1747, the move to TypeScript became much more feasible. While this PR mostly just adds types in a bunch of places, there are some changes to the code itself in order to minimize the number of casts and @ts-expect-errors that were require. I've tried to keep these kinds of changes to a minimum. I ended up converting a bunch of function expressions to arrow functions. Hopefully that doesn't add too much additional overhead when reviewing. There's still more cleanup that could be done (adding more missing types, converting all `var`s to either `const` or `let`, etc.), but I'm going to leave that for a future PR. Issue: None ## Test plan: - yarn test packages/kas/src Author: kevinb-khan Reviewers: kevinb-khan, handeyeco, jeremywiebe Required Reviewers: Approved By: handeyeco Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1748 --- .changeset/rare-pumas-rule.md | 5 + packages/kas/src/{nodes.js => nodes.ts} | 1434 ++++++++++++----------- 2 files changed, 751 insertions(+), 688 deletions(-) create mode 100644 .changeset/rare-pumas-rule.md rename packages/kas/src/{nodes.js => nodes.ts} (76%) 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;