"use strict"; const Postfix = {}; Postfix.eval = function(stack, operations, parseLiteral) { if (stack.peek() === null) throw Error("Empty expression stack."); const ops = operations, token = stack.pop(); if (!ops.contains(token)) return parseLiteral(token); const operation = ops.get(token), arity = operation.arity(); switch (arity) { case 0: return operation.apply(); case 1: const arg = Postfix.eval(stack, ops, parseLiteral); return operation.apply(arg); case 2: const arg2 = Postfix.eval(stack, ops, parseLiteral); const arg1 = Postfix.eval(stack, ops, parseLiteral); return operation.apply(arg1, arg2); default: throw Error("Invalid operation arity: " + airity); } } Postfix.parseStack = function(tokens, ops, lower, upper) { if (lower === undefined) lower = 0; if (upper === undefined) upper = tokens.length; if (upper - lower === 1) return new Stack([tokens[lower]]); for (let p = ops.minPrecedence(); p <= ops.maxPrecedence(); p++) { let j = -1; for (let i = upper - 1; i >= lower; i--) { const token = tokens[i]; if (!ops.contains(token)) continue; // token not an operator const operation = ops.get(token); console.assert(token === operation.operator()); if (operation.precedence() !== p) continue; if (operation.isPostfix()) { const lhs = Postfix.parseStack(tokens, ops, lower, i); return lhs.push(operation.operator()); } else if (operation.isInfix()) { const lhs = Postfix.parseStack(tokens, ops, lower, i); const rhs = Postfix.parseStack(tokens, ops, i + 1, upper); return lhs.pushStack(rhs).push(operation.operator()); } else if (operation.isPrefix()) j = i; } if (j >= 0) { const token = tokens[j], operation = ops.get(token); const rhs = Postfix.parseStack(tokens, ops, j + 1, upper); return rhs.push(operation.operator()); } } }; Object.freeze(Postfix); const Expr = {}; Expr.parseTokens = function(input) { if (Array.isArray(input)) return toStrings(input); else if (typeof input === "string") return toStrings(input.split(" ")); else if (Number.isFinite(input)) return toStrings([input]); else if (input === true || input === false) return toStrings([input]); throw Error("Invalid expression"); function toStrings(array) { // Returns a frozen array of strings. const strings = []; array.forEach((element) => strings.push(element.toString())); return Object.freeze(strings); } }; Expr.random = function(n, ops, randomTerm) { const opBagSize = // The number of operators will either be Number.isInteger(n) && n > 0 ? n : // the value of n or Expr.random.DEFAULT_OPERATOR_COUNT; // the default value. // PART I. COLLECT TOKENS let opBag = [], // Start with zero operations termBag = [randomTerm()]; // and one term. while (opBag.length < opBagSize) { // Repeat until the bag of operations is full: const op = ops.random(); // 1. Get the next random operation. const lhsIsArg = op.lhsIsArg(); // Note whether an operand is needed on the left. const rhsIsArg = op.rhsIsArg(); // Note whether an operand is needed on the right. if (lhsIsArg && rhsIsArg) { // 2. If operands are needed on both sides, termBag.unshift(randomTerm()); // then add another term to the bag of terms, opBag.push(op); // and store the operation in the bag. } // (Put term literals at the bottom of the bag.) else if (!(lhsIsArg || rhsIsArg)) // 3. Otherwise, if no operands are required, termBag.push(op.operator()); // then treat the operator as a term. else // 4. Otherwise, exactly one operand is needed, opBag.push(op); // so we only store the operation. } // PART II. SEQUENCE TOKENS let tokens = [], isComplete = false; // Initially, the expression is empty; it's incomplete. while (opBag.length > 0) { // Repeat until the bag of operation objects is empty: const op = opBag.pop(); // 1. Take an object out of the bag of operations. const lhsIsArg = op.lhsIsArg(); // Note whether an operand is needed on the left. const rhsIsArg = op.rhsIsArg(); // Note whether an operand is needed on the right. if (lhsIsArg) { // 2. If an operand is needed on the left, if (!isComplete) // and if the expression is not complete, tokens.push(termBag.pop()); // then we append a term to complete it. tokens.push(op.operator()); // Next, append the operator token. isComplete = !rhsIsArg; // The expression is complete if and only if a } // RHS operand is not required. else if (rhsIsArg) { // 3. But if we need a RHS (not a LHS) operand, if (!isComplete) // and if the expression is not complete, tokens.push(op.operator()); // then put the operator AT THE END; else // however, if the expression is complete, tokens.unshift(op.operator()); // then put the operator AT THE BEGINNING. } } // PART III. TERMINATE AND RETURN if (!isComplete) // If necessary, tokens.push(termBag.pop()); // add a term to terminate the series. return tokens.join(" "); // Return tokens representing a valid expression. }; Expr.random.DEFAULT_OPERATOR_COUNT = 8; Object.freeze(Expr); function GenericExpr(e, ops, parseLiteral) { const tokens = Expr.parseTokens(e); const postfixStack = Postfix.parseStack(tokens, ops); const postfixString = postfixStack.toString(); const value = Postfix.eval(postfixStack, ops, parseLiteral); this.valueOf = () => value; this.toArray = () => tokens; this.toString = (opt) => opt === "postfix" ? postfixString : e; Object.freeze(this); } Object.freeze(GenericExpr); function BooleanExpr(e) { GenericExpr.call(this, e, BooleanExpr.ops, BooleanExpr.parseLiteral); Object.freeze(this); } BooleanExpr.parseLiteral = (term) => term.toString(); BooleanExpr.random = (n) => Expr.random(n, BooleanExpr.ops, BooleanExpr.randomTerm); BooleanExpr.randomTerm = () => Math.random() < 0.5 ? "true" : "false"; BooleanExpr.ops = new Operations([ new Operation("&&", 0, true, true, (a, b) => MyMath.logical.and(a, b)), new Operation("||", 0, true, true, (a, b) => MyMath.logical.or(a, b)), new Operation("!", 1, false, true, (b) => MyMath.logical.not(b)) ]); Object.freeze(BooleanExpr); function NumericExpr(e) { GenericExpr.call(this, e, NumericExpr.ops, NumericExpr.parseLiteral); Object.freeze(this); } NumericExpr.parseLiteral = parseFloat; NumericExpr.MIN_TERM = -10; NumericExpr.MAX_TERM = 10; NumericExpr.random = (n) => Expr.random(n, NumericExpr.ops, NumericExpr.randomTerm); NumericExpr.randomTerm = () => Math.floor(NumericExpr.MIN_TERM + Math.random() * NumericExpr.MAX_TERM - NumericExpr.MIN_TERM); NumericExpr.ops = new Operations([ new Operation("+", 0, true, true, (a, b) => MyMath.add(a, b)), new Operation("-", 0, true, true, (a, b) => MyMath.sub(a, b)), new Operation("*", 1, true, true, (a, b) => MyMath.mul(a, b)), new Operation("/", 1, true, true, (a, b) => MyMath.div(a, b)), new Operation("%", 1, true, true, (a, b) => MyMath.rem(a, b)), new Operation("^", 2, true, true, (a, b) => MyMath.pow(a, b)), new Operation("!", 3, true, false, (a) => MyMath.factorial(a)), new Operation("neg", 3, false, true, (b) => MyMath.neg(b)), new Operation("sin", 3, false, true, (b) => MyMath.sin(b)), new Operation("cos", 3, false, true, (b) => MyMath.cos(b)), new Operation("PI", 4, false, false, () => MyMath.pi()) ]); Object.freeze(NumericExpr); const MyMath = Object.freeze({ pi: function() { return Math.PI; }, neg: function(b) { return -b; }, sin: function(b) { return Math.sin(b); }, cos: function(b) { return Math.cos(b); }, factorial: function(a) { return 1; }, // to do pow: function(a, b) { return Math.pow(a, b); }, mul: function(a, b) { return a * b; }, div: function(a, b) { return a / b; }, rem: function(a, b) { return a % b; }, add: function(a, b) { return a + b; }, sub: function(a, b) { return a - b; }, logical: { and: function(a, b) { return a && b; }, or: function(a, b) { return a || b; }, not: function(b) { return !b; } } }); function Operation(operator, precedence, lhsIsArg, rhsIsArg, f) { this.operator = () => operator; this.precedence = () => precedence; this.apply = f; this.lhsIsArg = () => lhsIsArg; this.rhsIsArg = () => rhsIsArg; this.isInfix = () => lhsIsArg && rhsIsArg; this.isPostfix = () => lhsIsArg && !rhsIsArg; this.isPrefix = () => !lhsIsArg && rhsIsArg; this.arity = () => (lhsIsArg ? 1 : 0) + (rhsIsArg ? 1 : 0); Object.freeze(this); } Object.freeze(Operation) function Operations(ops) { const minPrec = (function() { let min = undefined; ops.forEach(function(op) { const p = op.precedence(); if (min == undefined || p < min) min = p; }); return min; })(); const maxPrec = (function() { let max = undefined; ops.forEach(function(op) { const p = op.precedence(); if (max == undefined || p > max) max = p; }); return max; })(); const operatorMap = (function() { const map = Object.create(null); ops.forEach(function(op) { map[op.operator()] = op; }); return map; })(); this.contains = (operator) => operatorMap[operator] !== undefined; this.get = (operator) => operatorMap[operator]; this.minPrecedence = () => minPrec; this.maxPrecedence = () => maxPrec; this.random = () => ops[Math.floor(Math.random() * ops.length)]; Object.freeze(this); } Object.freeze(Operations); function Stack(elements) { const thisStack = this; this.pushStack = function (otherStack) { elements = elements.concat(otherStack.toString().split(" ")); return thisStack; }; this.push = function (element) { elements.push(element); return thisStack; }; this.pop = () => elements.pop(); this.toString = () => elements.join(" "); this.size = () => elements.length; this.peek = () => elements.length > 0 ? elements[elements.length - 1] : null; } Object.freeze(Stack);