Expressions

"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);