OOP Calculator

This next example, where we won’t rely on eval to do our calculations for us, will be a bit more involved. In order to keep things as simple as possible and to avoid redundancy, we are going to create a few custom objects with clear-cut responsibilities — for instance, we will want to avoid the piece of code responsible for doing the actual calculations having to know anything about how the calculator buttons work.

Let’s see what we need:

1.) At the heart of each mathematical expression are mathematical operations. They are all very similar to each other — they each take one (like sqr) or two inputs (like +) and somehow transform them to a numerical value — so it makes sense to base them all on the same object that handles that transformation from inputs to a numerical result. When instantiating such an Operation object, we can pass it some configuration object, so it will know what mathematical operation it actually represents.

2.) If a chain of mathematical operations could just be evaluated from left to right, we’d already be pretty much done here — for each new operation the user enters, we could just create a new Operation, and feed it the last operation as input. For instance, the user enters 1 + 2, that would result in an addition object with two inputs: addition(1, 2). If the user then enters * 3, we create a multiplication object, which we feed the addition as the first input, and the entered number as the second: multiplication(addition(1, 2), 3).

That’s not how it works, though, is it? Multiplication has a higher precedence than addition, which means that * forms a stronger bond with its surrounding numbers than + does. This means that the 2 that has been entered before doesn’t actually belong to that addition, but to the multiplication, so the result isn’t actually multiplication(addition(1, 2), 3) but addition(1, multiplication(2, 3)).

Different mathematical operations have different precedences, and we will need some sort of input stack that will hold all inputs for us and decide on its own, which has to be fed to which, so the precedences will be respected when the time comes for the Operations to do their thing.

3.) If we want to have a working calculator with buttons and all, we will have to set that up in the DOM, and add some click handlers, so they interact with those other components. This is the easy part, and it doesn’t really have anything to do with the actual calculations.

Operations

Alright, let’s get going. First of all, what exactly is different between various operations? All those differences, we are going to pack into neat little configuration objects.

The most obvious difference is the way they transform inputs into a result, meaning how they calculate. We can pack that information into a function, which takes the inputs and returns the result. Then, there’s the number of inputs needed to get a result, which can be 1 or 2. Let’s pick 2 as default, and mark the ones that only take 1. Quite importantly, each operation has a certain precedence value. That’s just an integer — the higher it is, the stronger it binds the surrounding numbers to itself. Each operation has a symbol we can print on the corresponding button, and a name we might need. Some operations will have certain validity restrictions — for instance, in a division, the second input can’t be 0. We can make that a function that takes the inputs and returns an appropriate error if something’s wrong, and false if everything’s alright. Lastly, it would be nice to have a pretty representation of the calculation we can print out to the user, so they know what’s going on. For that, we can use a function that takes the inputs and returns a pretty string.

And that’s about all the differences there are. Let’s take addition, division and square as examples, since with those three, all the differences listed will be relevant. Our configuration object then will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var operationData = {
  add: {
    precedence: 1,
    name: 'add',
    operation: function (a, b) {return a + b;},
    output: function (a, b) {return a + ' + ' + b;},
    buttonHTML: '+'
  },
  divide: {
    precedence: 2,
    name: 'divide',
    operation: function (a, b) {return a / b;},
    isInvalidInput: function (a, b) {return b == 0 ? 'division by 0' : false;},
    output: function (a, b) {return a + ' / ' + b;},
    buttonHTML: '/'
  },
  square: {
    precedence: 3,
    singleInput: true,
    name: 'square',
    operation: function (a) {return Math.pow(a, 2);},
    output: function (a) {return 'sqr(' + a + ')';},
    buttonHTML: 'x<sup>2</sup>'
  }
};

Secondly, what do all operations have in common? That’s what we are going to pack into the instantiable Operation object.

Most importantly, we have to be able to add inputs and to execute the operation. In order to be able to execute, the operation needs to know whether it already has all the inputs it needs — let’s call this state being saturated. Before it can execute, it will also have to check the inputs’ validity, so it won’t trip up on an illegal operation. Also, we decided we wanted to be able to get a string representation of the calculation.

That’s all we need our operation objects to do. It might be helpful to be able to convert the Operation to a number (the calculation result) or a string (the pretty representation) using the Number and String factories, so let’s also add a valueOf and a toString method.

All in all, the outline of the Operation object will be something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var Operation = function (options) {

  // There's no need for external code to know about the
  // operation's inputs, so let's make them local
  var inputs = [];

  // Make all the passed options accessible as object properties
  for (var key in options) {
    this[key] = options[key];
  };
 
  // Adding an input just puts it into the inputs array.
  // Before that, check whether the operation even needs another input, though.
  this.addInput = function (input) {
    if (this.isSaturated()) return this;
    inputs.push(input);
    return this;
  };

  // Check whether all the inputs are valid.
  // If no validation funtion has been passed, all inputs are valid.
  this.isInvalidInput = this.isInvalidInput || function () {return false;};

  // Check whether the operation already has all the inputs it needs
  this.isSaturated = function () {
    var inputCount = this.singleInput ? 1 : 2;
    for (var i = 0; i < inputCount; ++i) {
      if (inputs[i] == null || isNaN(inputs[i])) return false;
    }
    return true;
  };

  // Execute the operation, and put the result into the value property
  this.execute = function () {
    // Execution code
  };

  // Get a pretty representation of the calculation
  this.getCalculationString = function () {
    // Representation code
  };

  // Define the numerical value of the operation as its caclulation result
  // If there isn't a result yet, execute the operation first
  this.valueOf = function () {
    if (this.value == null) {
      this.execute();
    }
    return this.value;
  };

  // Define the string value of the operation as its pretty calculation string
  // If it isn't set yet, execute the operation first
  this.toString = function () {
    if (this.calculationString == null) {
      this.execute();
    }
    return this.getCalculationString();
  };

};

Okay, after figuring out what all operations have in common, this has been easy enough. Notice that whenever a method doesn’t have any interesting return value, we are just returning the object itself, which will allow us to chain methods later on, like this:

1
var onePlusTwo = new Operation(operationData.add).addInput(1).addInput(2).valueOf();

You might ask yourself why we even bother with setting those inputs separately, instead of just passing them to the execute method. The reason is that because of precedence handling, not all inputs will be immediately available. For instance, if the user enters 1 + 2, we can create an addition and set 1 as the first input, but we can’t set 2 as the second one yet, because the user might choose to enter * 3 after that, which means that the 2 actually belongs to the multiplication, and not to the addition.

Alright, what’s still missing is the code for the execute and getCalculationString methods. Let’s have a look at execute:

First, it needs to check whether all the inputs are already there. If not, there’s no reason to continue. If all the inputs are there but invalid, let’s just throw an appropriate error for now — we are going to catch that later. If all is in order, the actual calculation can take place, and the pretty calculation string can be set. That will look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Execute the operation, and put the result into the value property
this.execute = function () {
  // If execution has already failed once because of invalid inputs,
  // there's no need to try again, since inputs can only be added, but not changed.
  if (this.error) return this;
  // If inputs are missing, or the operation has already been executed,
  // there's no need to continue.
  if ( ! this.isSaturated() || this.value != null) return this;
  // Inputs don't have to be numbers — they can be other Operation objects too,
  // so for calculation purposes, map the inputs to their numerical values.
  var inputValues = inputs.map(function (input) {return Number(input);});
  // If an input is invalid, throw an error.
  // The error message is coming straight from the operation's configuration object
  // and should explain well enough what went wrong (e.g. 'division by 0').
  this.error = this.isInvalidInput.apply(this, inputValues);
  if (this.error) {
    throw new Error(this.error);
  }
  this.calculationString = this.getCalculationString();
  this.value = this.operation.apply(this, inputValues);
  return this;
};

this.operation.apply(this, inputValues) is where the actual calculation happens, with this.operation being the calculation function we passed to the constructor as an option.

Notice that the inputs don’t have to be numbers — we will want to nest operations, so the inputs can in fact also be other Operation objects. Since we made sure to implement a valueOf method, we can just map the inputs to their numerical values, though, and be done with it. (Array.map isn’t implemented in older browsers, so we’ll just add a custom implementation to the Array prototype later on, for backwards compatibility.)

As for the getCalculationString method:

At the heart of it, there will certainly be the output function we passed in as an option. A bit of a problem, though, is this: We want to be able to get a pretty calculation string at all times during user input, but because of precedence handling, most of the time most operations won’t be saturated yet. An easy solution is to just set up a parameter, so we can pass a missing input to the method, or pass an empty string, if the input simply isn’t there yet. For calculation purposes, that would be no good, but since the output method only does a bit of string concatenation, that’s perfectly alright.

If one of the inputs isn’t a number but another Operation, we’ll just get its string representation, and work with that.

Another thing that would be nice is to be able to not only get a string representing the whole calculation that has been entered so far, but to somehow collapse that string, so it reflects the current progress of the calculation. For example, if the full calculation string so far is 4 * 3 + 2 *, that first multiplication is already saturated and can be calculated, and it would be nice to get the collapsed string 12 + 2 *, so the user can see a simplified representation of what they have entered so far. We can easily make that happen by trying to execute the operation, and if it has been successful, just return the result, instead of the calculation string.

All in all, the getCalculationString method can look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Get a pretty representation of the calculation
this.getCalculationString = function (lastInput, collapsed) {
  // If collapsed string is requested, try to return the calculation result
  if (collapsed) {
    this.execute();
    if (this.value != null) return this.value.toString();
  }
  // Map all inputs to their string representations
  // regardless of whether they are numbers or Operation objects
  var singleInput = this.singleInput;
  var inputValues = inputs.map(function (input) {
    var inputValue = input.getCalculationString ?
      input.getCalculationString(lastInput, collapsed) :
      input.toString();
    // Single-input operations are already sporting parentheses
    // in their output, so if the inputValue has a pair too, remove them.
    return singleInput ? inputValue.replace(/^\((.*)\)$/g, '$1') : inputValue;
  });
  return options.output.apply(this, inputValues.concat([lastInput]));
};

We’ve got all we need now to work with our operations. Let’s take that 4 * 3 + 2 * calculation from before as an example — setting that incomplete calculation up, getting some output, adding the missing input, and getting the result will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
var m1 = new Operation(operationData.multiply).addInput(4).addInput(3);
var m2 = new Operation(operationData.multiply).addInput(2);
var a = new Operation(operationData.add).addInput(m1).addInput(m2);

console.log('Calculation string: ' + a.getCalculationString('[missing input]'));
console.log('Collapsed string: ' + a.getCalculationString('[missing input]', true));

m2.addInput(1);
console.log('Adding missing input ...');

console.log('Calculation string: ' + String(a));
console.log('Result: ' + Number(a));

Here’s a complete working example of the code we have so far. Click it to see the actual output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var Operation = function (options) {

  var inputs = [];

  for (var key in options) {
    this[key] = options[key];
  };
 
  this.addInput = function (input) {
    if (this.isSaturated()) return this;
    inputs.push(input);
    return this;
  };

  this.isInvalidInput = this.isInvalidInput || function () {return false;};

  this.isSaturated = function () {
    var inputCount = this.singleInput ? 1 : 2;
    for (var i = 0; i < inputCount; ++i) {
      if (inputs[i] == null || isNaN(inputs[i])) return false;
    }
    return true;
  };

  this.execute = function () {
    if (this.error) return this;
    if ( ! this.isSaturated() || this.value != null) return this;
    var inputValues = inputs.map(function (input) {return Number(input);});
    this.error = this.isInvalidInput.apply(this, inputValues);
    if (this.error) {
      throw new Error(this.error);
    }
    this.calculationString = this.getCalculationString();
    this.value = this.operation.apply(this, inputValues);
    return this;
  };

  this.getCalculationString = function (lastInput, collapsed) {
    if (collapsed) {
      this.execute();
      if (this.value != null) return this.value.toString();
    }
    var singleInput = this.singleInput;
    var inputValues = inputs.map(function (input) {
      var inputValue = input.getCalculationString ?
        input.getCalculationString(lastInput, collapsed) :
        input.toString();
      return singleInput ? inputValue.replace(/^\((.*)\)$/g, '$1') : inputValue;
    });
    return options.output.apply(this, inputValues.concat([lastInput]));
  };

  this.valueOf = function () {
    if (this.value == null) {
      this.execute();
    }
    return this.value;
  };

  this.toString = function () {
    if (this.calculationString == null) {
      this.execute();
    }
    return this.getCalculationString();
  };
};

var operationData = {
  add: {
    precedence: 3,
    name: 'add',
    operation: function (a, b) {return a + b;},
    output: function (a, b) {return a + ' + ' + b;},
    buttonHTML: '+'
  },
  multiply: {
    precedence: 2,
    name: 'multiply',
    operation: function (a, b) {return a * b;},
    output: function (a, b) {return a + ' * ' + b;},
    buttonHTML: '*'
  }
};


var m1 = new Operation(operationData.multiply).addInput(4).addInput(3);
var m2 = new Operation(operationData.multiply).addInput(2);
var a = new Operation(operationData.add).addInput(m1).addInput(m2);

console.log('Calculation string: ' + a.getCalculationString('[missing input]'));
console.log('Collapsed string: ' + a.getCalculationString('[missing input]', true));

m2.addInput(1);
console.log('Adding missing input ...');

console.log('Calculation string: ' + String(a));
console.log('Result: ' + Number(a));

Admittedly, what it’s able to do right now we could have gotten a lot cheaper, but a certain level of abstraction will pay off once we implement the precedence handling, which is what we’re going to do next.

Input Stack

Here comes the interesting part — we’re creating a component that keeps track of the user’s inputs, understands precedence handling, and knows when it’s okay to calculate partial results and when it has to wait for more input.

The first thing that’s noteworthy is that every user input consists of a number and a command. That’s just how a calculator works: The user enters a number into the input field, but that number will never be sent alone, because there’s simply no way of doing that. For the calculator to know when the user has finished entering the number, a button has to be pressed, and that button represents a command. Likewise, no command will ever be sent without a number, because the calculator’s input field will never be empty. At the very beginning, it will contain 0, and after each input it will be updated with a partial result. The only commands that are interesting for now are mathematical operations, which means that every input will consist of a number and the next operation.

What to do with the number? Whether it belongs to the new or the previous operation depends on which of those has the higher precedence. We just add it as input to the appropriate one. (If both have the same precedence, the left-to-right rule kicks in, and the number belongs to the previous operation.)

What to do with the operation? If the new operation’s precedence is equal to or lower than the previous one’s, the previous one is already saturated by now (we just filled its last input slot with a number), and we can just feed it as input to the new one — we’ve seen that before. Here’s an example:

1
2
3
4
5
6
7
Current input: 1 *
Input so far: 1 *
Internal representation: multiply(1, X)

Current input: 2 +
Input so far: 1 * 2 +
Internal representation: add(multiply(1, 2), X)

If the new operation’s precedence is higher than the previous one’s, then the previous one is still missing an input, so the best we can do is leave it for now, and add the new operation to the input stack:

1
2
3
4
5
6
7
Current input: 3 *
Input so far: 1 * 2 + 3 *
Input stack: add(multiply(1, 2), X), multiply(3, X)

Current input: 4 ^
Input so far: 1 * 2 + 3 * 4 ^
Input stack: add(multiply(1, 2), X), multiply(3, X), power(4, X)

So, if a new operation comes along, and we can’t yet resolve the previous one because it’s still missing something, we’re just going to push it into an array, in order to keep track of the whole calculation. The reason I’m calling this array a stack is the fact that we are only ever going to be interested in the topmost operation (the one that has been pushed on the stack last) — that’s the one whose precedence decides what to do with any new operations.

Alright, so, what happens if the next input is a lower-precedence operation? Let’s take care of the number first — like in the first example, it belongs to the previous operation:

1
2
3
4
Current input: 5 *
Input so far: 1 * 2 + 3 * 4 ^ 5 *
Input stack: add(multiply(1, 2), X), multiply(3, X), power(4, 5)
(new operation not yet added to input stack)

What to do with the new operation, though? The thing is, whatever follows after (1 * 2) + 3 * (4 ^ 5) * (parentheses added for already saturated operations) can’t in any way influence 3 * (4 ^ 5) any more, because * doesn’t have higher precedence than any of those other operations in there. That means that we might as well feed the last operation on the stack to the one before:

1
Input stack: add(multiply(1, 2), X), multiply(3, power(4, 5))

We’ll call the thing we just did collapsing the stack. Can we do it once again? Let’s see: Can whatever follows after (1 * 2) + [3 * (4 ^ 5)] * (brackets added for the already collapsed part) in any way influence (1 * 2) + [3 * (4 ^ 5)]? Yes it can, because * binds [3 * (4 ^ 5)] stronger than + does. So, we’ve done all the collapsing we can do, and the only thing left is to feed the last operation on the stack to the new one, just as we did in the first example:

1
Input stack: add(multiply(1, 2), X), multiply(multiply(3, power(4, 5), X)

If the user enters another number and hits =, that’s the end of the calculation, which means that we can just collapse the whole input stack and end up with a result:

1
2
3
Current input: 6 =
Input so far: 1 * 2 + 3 * 4 ^ 5 * 6 =
Input stack: add(multiply(1, 2), multiply(multiply(3, power(4, 5), 6))

And that’s pretty much it.

One more thing is missing: We would like to be able use parentheses in order to override precedences. That’s no biggie, though, since opening a parenthesis just opens a new context, inside of which all precedences are handled as usual (and without any care about what’s going on outside of that new context). That means that it suffices to just open up a new input stack on top of the main one. If several parentheses are open at the same time, we’ll end up with several levels of input stacks. If one is closed, the input stack on the topmost level can be collapsed into a single operation, which can then be pushed onto the stack a level below.

We’ve got everything we need now to start coding. We want that input handling component to be just one object with a neat public interface we can use to push inputs to the stack — we don’t want any other parts of the code to have to care about how it does what it does — so it makes sense to make it a singleton using the module pattern (I’m explaining that concept in greater detail in the article about closures). And although the input handling component will actually have to manage several different stacks internally, I am just going to call it InputStack, because from the outside we’re just interested in what it does, and not in how it does it.

What do we need from the public interface? We need to be able to push inputs (consisting of a number and an operation), open and close contexts (which means using parentheses in the calculation), and evaluate what’s currently in there (which means calculating the actual result). Then we will need a way to get some output: First of all, we need to be able to get a partial result: if you play around with a calculator, you’ll notice that you won’t only get a result in the end — every time some part of your calculation already yields a result, you’ll get that. We won’t need another method for the end result — as soon as all contexts are closed and the remaining stack is fully collapsed, the partial result will automatically be the end result. Secondly, we will want to get the calculation string — as mentioned in the part about the Operation objects both the full string, and the collapsed one.

In order to make that work, what do we need to happen internally? That question is easily answered by looking at that example calculation above: We need to keep track of various levels of input stacks in some data structure. That’s basically an array of context levels, where each context level is an array of operations. Since we’re using those arrays as stacks (meaning we only ever manipulate the last entry), let’s make a custom Stack object that inherits from Array, and add a peek method that returns its last element. Then we need to be able to collapse the input stack. We will also need a way to feed the topmost operation in the stack to another operation, which basically means to wrap the topmost operation in the other one. If this input component is supposed to handle more than just one calculation in its lifetime, we will also have to be able to reset it after a calculation is finished.

So, the basic outline will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
var InputStack = (function () {
 
  // Data structure to keep track of contexts and operations
  var levels;

  // If a context has just been closed, store its value here.
  // On the next input, the input's number will be discarded
  // and the closed context's value will be used instead.
  var closedContext;

  // Whenever something can be calculated already, it will end up here.
  var partialResult;

  // Whenever an Operation object throws an error, we are going to catch it and
  // put it here, so we'll know that it doesn't make sense to continue the calculation
  var error;

  // Stack object. Just an array with a peek function.
  var Stack = function () {
    this.peek = function () {return this[this.length - 1];};
  };
  Stack.prototype = [];
 
  // Initialize the stack for managing context levels
  // and put in the first context level (which is a stack for managing operations).
  // We're at the beginning of a new calculation now,
  // so there are no open parentheses or errors yet.
  var reset = function () {
    levels = new Stack;
    levels.push(new Stack);
    closedContext = error = null;
  };
 
  // Feed the last operation to the new one, and put
  // the new one into the last one's place on the stack
  var wrapLastOperation = function (operation) {
    var stack = levels.peek();
    stack.push(operation.addInput(stack.pop()));
    // Since there has been a change to the stack, try to collapse it
    collapse(operation.precedence);
  };

  // Collapse the current context as far as possible.
  // In order to figure out how far it can be collapsed,
  // it needs to know the next operation's precedence.
  var collapse = function (precedence) {
    // Collapse code
  };

  // Initialize the data structure
  reset();


  return {
    // Push a number and the next operation to the current stack
    push: function (number, operation) {
    },
    // Open a new context (means: add an opening parenthesis to the calculation)
    openContext: function () {
    },
    // Close the last context (means: add a closing parenthesis to the calculation)
    closeContext: function (number) {
    },
    // Calculate the end result
    evaluate: function (number) {
    },
    // Get a partial result for output in the calculator's number field.
    // The "reset" function resets everything except the partial result — this is done here.
    getPartialResult: function () {
      var _partialResult = partialResult;
      partialResult = 0;
      return _partialResult;
    },
    // Get a pretty string representing the calculation so far
    getCalculationString: function (collapsed) {
    }
  };
 
}());

Easy enough, so far. A note regarding that closedContext variable: Whenever a context is closed, that whole context level will be completely shut down, and the calculation will continue on the level below, where the value of that context that has just been closed and evaluated will be used instead of the user input’s number (as always, a number will be sent, but after a closing parenthesis, it doesn’t make any sense mathematically, so it will be discarded). Since we have to wait for the next user input in order to know where that closed context’s value will actually go (again, precedence handling), we’ll just store it in that local variable in the meantime. More on that later.

Alright, the code I’ve already put in there should be pretty clear. The code for the collapse function is still missing, and so is the code for pretty much all the public methods. Let’s try to flesh those out one by one:

The collapse function:

From the example above, we already know what to do: Pop the last operation off the stack, and check whether the operation that came before has lower precedence. If so, collapse, meaning put the popped off operation into the last one as input. Repeat as long as that precedence condition is satisfied. If the condition fails, just push the popped off operation back on the stack. During that process, we should also update the partial result, since collapsing parts of the stack means that new stuff can be calculated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var collapse = function (precedence) {
 
  // Pop off the last operation, and fetch the previous one,
  // so we can have a look at its precedence
  var stack = levels.peek();
  var currentOperation = stack.pop();
  var previousOperation = stack.peek();
 
  // If the stack was empty, nothing needs to be done
  if ( ! currentOperation) return;

  // If the popped off operation doesn't yet have all the inputs it needs,
  // just push it back on the stack and return.
  if ( ! currentOperation.isSaturated()) {
    stack.push(currentOperation);
    return;
  }
 
  // If code execution makes it here, that means we have a saturated
  // operation on our hands, so let's try to fetch its result.
  // If the operation throws an error because of invalid inputs,
  // catch it and put the error message in both "partialResult" for output purposes,
  // and in "error", so other methods will know not to continue this failed calculation.
  try {
    partialResult = Number(currentOperation);
  }
  catch (e) {
    partialResult = error = 'Error: ' + e.message;
  }

  // If there is a previous operation on the stack, and it doesn't have
  // lower precedence than the popped off one, feed the popped off one to it.
  // That's the actual "collapsing" part right here.
  if (previousOperation && previousOperation.precedence >= precedence) {
    previousOperation.addInput(currentOperation);
    // The stack has one less element now — let's try to do that again.
    collapse(precedence);
  }
  // If not, just push the popped off operation back on the stack
  else {
    stack.push(currentOperation);
  }

};

As for the public methods: Let’s try to return this whenever possible, so we can chain them.

First, the push method:

There’s not much to say here — it does exactly as described in the examples: depending on precedence, it feeds the number to the appropriate operation, and then either pushes the operation to the stack, or wraps it around the last one. Here, the closedContext variable mentioned earlier comes into play: if it’s set, a context has just been closed before, so the number parameter is irrelevant (it doesn’t make any mathematical sense after the closing parenthesis) and the context’s value is used instead.

What’s noteworthy here is that calculators sometimes do it the other way around: If a number is added after a closing parenthesis, some calculators replace the expression inside the parentheses with the number, instead of discarding the number (my Windows calculator does that). My Android calculator, on the other hand, automatically places a multiplication sign between the closing parenthesis and the number. All those ways are okay too — it just comes down to how you want to handle inputs that don’t make immediate mathematical sense.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
  push: function (number, operation) {

    // If the current calculation has already thrown an error, start from scratch.
    error && reset();

    // Fetch the last operation in the current context.
    // That's the only one that's relevant for precedence handling.
    var stack = levels.peek();
    var lastOperation = stack.peek();

    // If a context has just been closed, discard the number from the
    // calculator's input field, and use the value of the closed context instead.
    var input = closedContext || number;
    closedContext = null;
   
    // Set the partial result. If parts of the stack can be collapsed later,
    // this will be overwritten with more interesting results.
    partialResult = Number(input);
   
    // If the stack is empty, or the new operation has higher precedence
    // than the last one, the new operation binds the input to itself.
    // So, just put it in there, and push the operation to the stack.
    if ( ! lastOperation || operation.precedence > lastOperation.precedence) {
      stack.push(operation.addInput(input));
      // The new operation might already be saturated
      // (in case it takes only one input), so try to collapse the stack.
      collapse(operation.precedence);
    }
    // If not, the input belongs to the last operation.
    else {
      lastOperation.addInput(input);
      // The last operation is now saturated, so try to collapse.
      collapse(operation.precedence);
      // Whatever turns out to be the topmost operation on the stack now
      // needs to be fed to the new operation, which will take its place.
      wrapLastOperation(operation);
    }

    return this;
   
  }
}

Let’s take the openContext, closeContext and evaluate functions on in one go:

I commented all the code, and it should be pretty self-explanatory what’s going on. What’s probably worth mentioning is that each Operation object is capable of providing its own pretty calculation string, but we don’t yet have a way of displaying parentheses around a closed context. That’s easily done, though, by introducing a new “context” operation that doesn’t do anything except adding parentheses to the output. The configuration object of that new operation will look like this:

1
2
3
4
5
6
7
8
9
var operationData = {
  context: {
    precedence: 4,
    singleInput: true,
    name: 'context',
    operation: function (a) {return a;},
    output: function (a) {return '(' + a + ')';}
  }
}

I gave it the highest precedence value of all operations, because parentheses bind their contents stronger than any surrounding operations — but the way we will use that operation, it will always be saturated before landing on the stack, so its precedence is actually irrelevant.

And here are those three methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
{
  openContext: function () {
 
    // If the current calculation has already thrown an error, start from scratch.
    error && reset();

    var lastOperation = levels.peek().peek();
   
    // If a context has just been closed, opening another one
    // before putting in a mathematical operation first doesn't make any sense.
    // Likewise, if the last operation already has all the inputs it needs,
    // opening a new context won't do any good.
    if (closedContext || lastOperation && lastOperation.isSaturated()) return;

    // Opening a new context means pushing a new operations stack to the levels stack
    levels.push(new Stack);

    return this;
  },
 
  closeContext: function (number) {

    // If the current calculation has already thrown an error, start from scratch.
    error && reset();

    // If there's only the main level left, there's no context to close
    if (levels.length <= 1) return;

    // If another context has just been closed, discard the inputted number
    var input = closedContext || number;
   
    // Try to fetch the last operation from the stack
    var stack = levels.peek();
    var lastOperation = stack.peek();
   
    // Actually close the context and store the result away.
    // Closing the context is done by collapsing the topmost stack
    // into a single operation or number, and feeding that to a new "context" operation,
    // which does nothing except adding parentheses to the output.
    closedContext = new Operation(operationData.context).addInput(
      lastOperation ?
        // If the context's stack isn't empty, feed the input to the last operation, and collapse.
        // After that, there will only be one operation left on the context's stack,
        // which will be the input for the "context" operation.
        (function () {
          lastOperation.addInput(input);
          // Passing 0 precedence makes sure that the whole stack will be collapsed.
          collapse(0);
          return stack.pop();
        }())
        // If the stack is empty, that means there's just a single number in the parentheses.
        // In that case, that number is the input for the "context" operation.
        : input
    );

    // The closed context has everything it needs, so fetch its result
    partialResult = Number(closedContext);

    // The context has been handled now, and its result stored away,
    // so its input stack can be popped off the levels stack.
    // The calculation will continue on the level below.
    levels.pop();

    return this;
  },

  evaluate: function (number) {

    // If the current calculation has already thrown an error, start from scratch.
    error && reset();

    // If another context has just been closed, discard the inputted number
    var input = closedContext || number;

    // In case there are no operations on the stack at all, and the user just
    // entered one number and hit "=", set the result to that number.
    // If there's more, it will be overwritten later, while collapsing.
    partialResult = Number(input);

    // In case the user hit "=" whithout closing all the parentheses,
    // close all open contexts now.
    while (levels.length > 1) {
      this.closeContext(input);
    }

    // If there actually is an operation on the stack, feed it
    var lastOperation = levels.peek().peek();
    lastOperation && lastOperation.addInput(input);

    // Collapse the stack. Passing in the lowest possible precedence value
    // makes sure that the whole stack will be collapsed.
    collapse(0);

    // By now, there's an end result stored away in partialResult.
    // We don't need the calculation data that led to that result
    // any more, so let's just reset all the stacks.
    reset();
   
    return this;
  }
}

All that’s missing is the getCalculationString method:

Getting the calculation string is very much like collapsing the stack: We start at the topmost operation in the topmost context, get its string, feed it to the previous one, and then do the same for that one. And so on. When we have reached the bottommost operation in the bottommost context, we end up with a string representing the full calculation.

Two things need to be mentioned:

First, that topmost operation we start with is probably still missing an input — that’s okay, though, because for display purposes we can just leave that empty. We just have to make sure not to forget that if a context has just been closed before, its result is supposed to be used as input on the level below, once the next operation kicks in, so when getting the string, we will also have to use that closed context as the missing input instead of leaving it empty.

Second, we’ve introduced that context operation for output purposes, so closed contexts come with their own sets of parentheses. As long as they are still open, though, they won’t, so let’s make sure to add an opening parenthesis to the calculation string for each active context level.

Here we go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  getCalculationString: function (collapsed) {
 
    // If a context has just been closed, that's the missing input.
    // Else leave it empty.
    var result = closedContext ? closedContext.getCalculationString('', collapsed) : '';

    // Start with topmost context and work to the bottom
    for (var j = levels.length - 1; j >= 0; --j) {
     
      // Start with topmost operation and work to the bottom
      for (var i = levels[j].length - 1; i >= 0; --i) {
       
        // Feed the string we already got to the next function, so it will be wrapped
        result = levels[j][i].getCalculationString(result, collapsed);
      }

      // For each open context, add an opening parenthesis.
      // (Except for the bottommost one — the whole calculation doesn't need parentheses.)
      if (j > 0) {
        result = '(' + result;
      }
    }
   
    return result;
  }
}

Phew.

That’s it, we got it all working now. We still don’t have any UI, of course, but in order to test the calculator, we don’t need that — we can just use the public interface of the input handler.

As an example, let’s take the following calculation:

1
(6 + 5 * 4 - 3) - sqr(3) * 2 =

A bit of precedence handling, a separate context, an operation that takes only one input — that should be enough to give us an idea. Using the interface we just wrote, this translates to the following pseudocode:

1
2
3
4
5
6
7
8
9
openContext();
push(6, +);
push(5, *);
push(4, -)
closeContext(3);
push([number irrelevant], -);
push(3, sqr);
push([number irrelevant], *)
evaluate(2);

Let’s see that in action. Click to actually see the output. You can edit the code, too — I’ve put in a few more operations to play with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
var operationData = {
  add: {
    precedence: 1,
    name: 'add',
    operation: function (a, b) {return a + b;},
    output: function (a, b) {return a + ' + ' + b;},
    buttonHTML: '+'
  },
  subtract: {
    precedence: 1,
    name: 'subtract',
    operation: function (a, b) {return a - b;},
    output: function (a, b) {return a + ' - ' + b;},
    buttonHTML: '-'
  },
  multiply: {
    precedence: 2,
    name: 'multiply',
    operation: function (a, b) {return a * b;},
    output: function (a, b) {return a + ' * ' + b;},
    buttonHTML: '*'
  },
  divide: {
    precedence: 2,
    name: 'divide',
    operation: function (a, b) {return a / b;},
    isInvalidInput: function (a, b) {return b == 0 ? 'division by 0' : false;},
    output: function (a, b) {return a + ' / ' + b;},
    buttonHTML: '/'
  },
  epow: {
    precedence: 3,
    singleInput: true,
    name: 'epow',
    operation: function (a) {return Math.pow(Math.E, a);},
    output: function (a, b) {return 'epow(' + a + ')';},
    buttonHTML: 'e<sup>x</sup>'
  },
  ln: {
    precedence: 3,
    singleInput: true,
    name: 'ln',
    operation: function (a) {return Math.log(a);},
    isInvalidInput: function (a) {return a <= 0 ? 'ln of non-positive number' : false;},
    output: function (a, b) {return 'ln(' + a + ')';},
    buttonHTML: 'ln'
  },
  square: {
    precedence: 3,
    singleInput: true,
    name: 'square',
    operation: function (a) {return Math.pow(a, 2);},
    output: function (a) {return 'sqr(' + a + ')';},
    buttonHTML: 'x<sup>2</sup>'
  },
  squareRoot: {
    precedence: 3,
    singleInput: true,
    name: 'squareRoot',
    operation: function (a) {return Math.sqrt(a);},
    isInvalidInput: function (a) {return a < 0 ? 'complex number' : false;},
    output: function (a) {return 'sqrt(' + a + ')';},
    buttonHTML: '&#8730;'
  },
  power: {
    precedence: 3,
    name: 'power',
    operation: function (a, b) {return Math.pow(a, b);},
    isInvalidInput: function (a, b) {return isNaN(Math.pow(a, b)) ? 'complex number' : false;},
    output: function (a, b) {return a + ' ^ ' + b;},
    buttonHTML: 'x<sup>y</sup>'
  },
  yroot: {
    precedence: 3,
    name: 'yroot',
    operation: function (a, b) {return Math.pow(a, b == 0 ? 0 : 1 / b);},
    isInvalidInput: function (a, b) {
      return isNaN(Math.pow(a, a, b == 0 ? 0 : 1 / b)) ? 'complex number' : false;
    },
    output: function (a, b) {return a + ' yroot ' + b;},
    buttonHTML: '<sup>y</sup>&#8730;'
  },
  modulo: {
    precedence: 2,
    name: 'modulo',
    operation: function (a, b) {return a % b;},
    output: function (a, b) {return a + ' % ' + b;},
    buttonHTML: 'Mod'
  },
  negate: {
    precedence: 3,
    singleInput: true,
    name: 'negate',
    operation: function (a) {return -a;},
    output: function (a) {return 'negate(' + a + ')';},
    buttonHTML: '&#177;'
  },
  reciproc: {
    precedence: 3,
    singleInput: true,
    name: 'reciproc',
    operation: function (a) {return 1 / a;},
    isInvalidInput: function (a) {return a == 0 ? 'division by 0' : false;},
    output: function (a) {return 'reciproc(' + a + ')';},
    buttonHTML: '1/x'
  },
  context: {
    precedence: 4,
    singleInput: true,
    name: 'context',
    operation: function (a) {return a;},
    output: function (a) {return '(' + a + ')';}
  }
};



var Operation = function (options) {

  var inputs = [];

  for (var key in options) {
    this[key] = options[key];
  };
 
  this.addInput = function (input) {
    if (this.isSaturated()) return this;
    inputs.push(input);
    return this;
  };

  this.isInvalidInput = this.isInvalidInput || function () {return false;};

  this.isSaturated = function () {
    var inputCount = this.singleInput ? 1 : 2;
    for (var i = 0; i < inputCount; ++i) {
      if (inputs[i] == null || isNaN(inputs[i])) return false;
    }
    return true;
  };

  this.execute = function () {
    if (this.error) return this;
    if ( ! this.isSaturated() || this.value != null) return this;
    var inputValues = inputs.map(function (input) {return Number(input);});
    this.error = this.isInvalidInput.apply(this, inputValues);
    if (this.error) {
      throw new Error(this.error);
    }
    this.calculationString = this.getCalculationString();
    this.value = this.operation.apply(this, inputValues);
    return this;
  };

  this.getCalculationString = function (lastInput, collapsed) {
    if (collapsed) {
      this.execute();
      if (this.value != null) return this.value.toString();
    }
    var singleInput = this.singleInput;
    var inputValues = inputs.map(function (input) {
      var inputValue = input.getCalculationString ?
        input.getCalculationString(lastInput, collapsed) :
        input.toString();
      return singleInput ? inputValue.replace(/^\((.*)\)$/g, '$1') : inputValue;
    });
    return options.output.apply(this, inputValues.concat([lastInput]));
  };

  this.valueOf = function () {
    if (this.value == null) {
      this.execute();
    }
    return this.value;
  };

  this.toString = function () {
    if (this.calculationString == null) {
      this.execute();
    }
    return this.getCalculationString();
  };
};



var InputStack = (function () {
 
  var levels, closedContext, partialResult, error;

  var Stack = function () {
    this.peek = function () {return this[this.length - 1];};
  };
  Stack.prototype = [];
 
  var reset = function () {
    levels = new Stack;
    levels.push(new Stack);
    closedContext = error = null;
  };

  var wrapLastOperation = function (operation) {
    var stack = levels.peek();
    stack.push(operation.addInput(stack.pop()));
    collapse(operation.precedence);
  };

  var collapse = function (precedence) {
    var stack = levels.peek();
    var currentOperation = stack.pop();
    var previousOperation = stack.peek();
   
    if ( ! currentOperation) return;

    if ( ! currentOperation.isSaturated()) {
      stack.push(currentOperation);
      return;
    }
   
    try {
      partialResult = Number(currentOperation);
    }
    catch (e) {
      partialResult = error = 'Error: ' + e.message;
    }

    if (previousOperation && previousOperation.precedence >= precedence) {
      previousOperation.addInput(currentOperation);
      collapse(precedence);
    }
    else {
      stack.push(currentOperation);
    }
  };

  reset();

  return {
    push: function (number, operation) {
      error && reset();
      var stack = levels.peek();
      var lastOperation = stack.peek();
      var input = closedContext || number;
      closedContext = null;
      partialResult = Number(input);
      if ( ! lastOperation || operation.precedence > lastOperation.precedence) {
        stack.push(operation.addInput(input));
        collapse(operation.precedence);
      }
      else {
        lastOperation.addInput(input);
        collapse(operation.precedence);
        wrapLastOperation(operation);
      }
      return this;
    },
    openContext: function () {
      error && reset();
      var lastOperation = levels.peek().peek();
      if (closedContext || lastOperation && lastOperation.isSaturated()) return;
      levels.push(new Stack);
      return this;
    },
    closeContext: function (number) {
      error && reset();
      if (levels.length <= 1) return;
      var input = closedContext || number;
      var stack = levels.peek();
      var lastOperation = stack.peek();
      closedContext = new Operation(operationData.context).addInput(
        lastOperation ? (function () {
          lastOperation.addInput(input);
          collapse(0);
          return stack.pop();
        }()) : input
      );
      partialResult = Number(closedContext);
      levels.pop();
      return this;
    },
    evaluate: function (number) {
      error && reset();
      var input = closedContext || number;
      partialResult = Number(input);
      while (levels.length > 1) {
        this.closeContext(input);
      }
      var lastOperation = levels.peek().peek();
      lastOperation && lastOperation.addInput(input);
      collapse(0);
      reset();
      return this;
    },
    getPartialResult: function () {
      var _partialResult = partialResult;
      partialResult = 0;
      return _partialResult;
    },
    getCalculationString: function (collapsed) {
      var result = closedContext ? closedContext.getCalculationString('', collapsed) : '';
      for (var j = levels.length - 1; j >= 0; --j) {
        for (var i = levels[j].length - 1; i >= 0; --i) {
          result = levels[j][i].getCalculationString(result, collapsed);
        }
        if (j > 0) {
          result = '(' + result;
        }
      }
      return result;
    }
  };
 
}());



function output(input) {
  console.log('----------------------------------------------------------');
  console.log('                 Input: "' + input + '"');
  console.log('      Full calculation: ' + InputStack.getCalculationString());
  console.log(' Collapsed calculation: ' + InputStack.getCalculationString(true));
  console.log('        Partial result: ' + InputStack.getPartialResult());
};

output('(', InputStack.openContext());
output('6 +', InputStack.push(6, new Operation(operationData.add)));
output('5 *', InputStack.push(5, new Operation(operationData.multiply)));
output('4 -', InputStack.push(4, new Operation(operationData.subtract)));
output('3)', InputStack.closeContext(3));
output('-', InputStack.push(null, new Operation(operationData.subtract)));
output('3 [sqr]', InputStack.push(3, new Operation(operationData.square)));
output('*', InputStack.push(null, new Operation(operationData.multiply)));
output('2 =', InputStack.evaluate(2));

The Finished Calculator

Setting up the user interface is a bit boring, really, so I won’t go into any detail on that. Just add input and output fields and a few buttons to the DOM. When the buttons are clicked, tell the input stack about it, and fetch the output.

It might be nice to also add some convenience stuff, like buttons for deleting (parts of) the input, or for storing away a value for later use. Being able to enter numbers and operations with the keyboard would be nice, too. I’ve added those things in the finished code below. Since all of this comes down to just a bit of DOM manipulation, I used jQuery for this part.

All we have to do to finish things up is wrap our code in a Calculator component, so it won’t pollute the global namespace and can just be dropped into any page. As long as there’s a <div> with the id “calculator” somewhere in the HTML, it will be transformed into the calculator we built.

Here’s the finished calculator — click to see it in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>OOP Calculator</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
<script type="text/javascript">

$(function () {


  var Calculator = (function () {

    var operationData = {
      add: {
        precedence: 1,
        name: 'add',
        operation: function (a, b) {return a + b;},
        output: function (a, b) {return a + ' + ' + b;},
        buttonHTML: '+'
      },
      subtract: {
        precedence: 1,
        name: 'subtract',
        operation: function (a, b) {return a - b;},
        output: function (a, b) {return a + ' - ' + b;},
        buttonHTML: '-'
      },
      multiply: {
        precedence: 2,
        name: 'multiply',
        operation: function (a, b) {return a * b;},
        output: function (a, b) {return a + ' * ' + b;},
        buttonHTML: '*'
      },
      divide: {
        precedence: 2,
        name: 'divide',
        operation: function (a, b) {return a / b;},
        isInvalidInput: function (a, b) {return b == 0 ? 'division by 0' : false;},
        output: function (a, b) {return a + ' / ' + b;},
        buttonHTML: '/'
      },
      epow: {
        precedence: 4,
        singleInput: true,
        name: 'epow',
        operation: function (a) {return Math.pow(Math.E, a);},
        output: function (a, b) {return 'epow(' + a + ')';},
        buttonHTML: 'e<sup>x</sup>'
      },
      ln: {
        precedence: 4,
        singleInput: true,
        name: 'ln',
        operation: function (a) {return Math.log(a);},
        isInvalidInput: function (a) {return a <= 0 ? 'ln of non-positive number' : false;},
        output: function (a, b) {return 'ln(' + a + ')';},
        buttonHTML: 'ln'
      },
      square: {
        precedence: 4,
        singleInput: true,
        name: 'square',
        operation: function (a) {return Math.pow(a, 2);},
        output: function (a) {return 'sqr(' + a + ')';},
        buttonHTML: 'x<sup>2</sup>'
      },
      squareRoot: {
        precedence: 4,
        singleInput: true,
        name: 'squareRoot',
        operation: function (a) {return Math.sqrt(a);},
        isInvalidInput: function (a) {return a < 0 ? 'complex number' : false;},
        output: function (a) {return 'sqrt(' + a + ')';},
        buttonHTML: '&#8730;'
      },
      power: {
        precedence: 3,
        name: 'power',
        operation: function (a, b) {return Math.pow(a, b);},
        isInvalidInput: function (a, b) {return isNaN(Math.pow(a, b)) ? 'complex number' : false;},
        output: function (a, b) {return a + ' ^ ' + b;},
        buttonHTML: 'x<sup>y</sup>'
      },
      yroot: {
        precedence: 3,
        name: 'yroot',
        operation: function (a, b) {return Math.pow(a, b == 0 ? 0 : 1 / b);},
        isInvalidInput: function (a, b) {
          return isNaN(Math.pow(a, a, b == 0 ? 0 : 1 / b)) ? 'complex number' : false;
        },
        output: function (a, b) {return a + ' yroot ' + b;},
        buttonHTML: '<sup>y</sup>&#8730;'
      },
      modulo: {
        precedence: 2,
        name: 'modulo',
        operation: function (a, b) {return a % b;},
        output: function (a, b) {return a + ' % ' + b;},
        buttonHTML: 'Mod'
      },
      negate: {
        precedence: 4,
        singleInput: true,
        name: 'negate',
        operation: function (a) {return -a;},
        output: function (a) {return 'negate(' + a + ')';},
        buttonHTML: '&#177;'
      },
      reciproc: {
        precedence: 4,
        singleInput: true,
        name: 'reciproc',
        operation: function (a) {return 1 / a;},
        isInvalidInput: function (a) {return a == 0 ? 'division by 0' : false;},
        output: function (a) {return 'reciproc(' + a + ')';},
        buttonHTML: '1/x'
      },
      context: {
        precedence: 5,
        singleInput: true,
        name: 'context',
        operation: function (a) {return a;},
        output: function (a) {return '(' + a + ')';}
      }
    };



   

    var Operation = function (options) {
   
      var inputs = [];
   
      for (var key in options) {
        this[key] = options[key];
      };
     
      this.addInput = function (input) {
        if (this.isSaturated()) return this;
        inputs.push(input);
        return this;
      };
   
      this.isInvalidInput = this.isInvalidInput || function () {return false;};
   
      this.isSaturated = function () {
        var inputCount = this.singleInput ? 1 : 2;
        for (var i = 0; i < inputCount; ++i) {
          if (inputs[i] == null || isNaN(inputs[i])) return false;
        }
        return true;
      };
   
      this.execute = function () {
        if (this.error) return this;
        if ( ! this.isSaturated() || this.value != null) return this;
        var inputValues = inputs.map(function (input) {return Number(input);});
        this.error = this.isInvalidInput.apply(this, inputValues);
        if (this.error) {
          throw new Error(this.error);
        }
        this.calculationString = this.getCalculationString();
        this.value = this.operation.apply(this, inputValues);
        return this;
      };
   
      this.getCalculationString = function (lastInput, collapsed) {
        if (collapsed) {
          this.execute();
          if (this.value != null) return this.value.toString();
        }
        var singleInput = this.singleInput;
        var inputValues = inputs.map(function (input) {
          var inputValue = input.getCalculationString ?
            input.getCalculationString(lastInput, collapsed) :
            input.toString();
          return singleInput ? inputValue.replace(/^\((.*)\)$/g, '$1') : inputValue;
        });
        return options.output.apply(this, inputValues.concat([lastInput]));
      };
   
      this.valueOf = function () {
        if (this.value == null) {
          this.execute();
        }
        return this.value;
      };

      this.toString = function () {
        if (this.calculationString == null) {
          this.execute();
        }
        return this.getCalculationString();
      };
    };
   



   
    var InputStack = (function () {
     
      var levels, closedContext, partialResult, error;

      var Stack = function () {
        this.peek = function () {return this[this.length - 1];};
      };
      Stack.prototype = [];
     
      var reset = function () {
        levels = new Stack;
        levels.push(new Stack);
        closedContext = error = null;
      };

      var wrapLastOperation = function (operation) {
        var stack = levels.peek();
        stack.push(operation.addInput(stack.pop()));
        collapse(operation.precedence);
      };

      var collapse = function (precedence) {
        var stack = levels.peek();
        var currentOperation = stack.pop();
        var previousOperation = stack.peek();
       
        if ( ! currentOperation) return;

        if ( ! currentOperation.isSaturated()) {
          stack.push(currentOperation);
          return;
        }
       
        try {
          partialResult = Number(currentOperation);
        }
        catch (e) {
          partialResult = error = 'Error: ' + e.message;
        }

        if (previousOperation && previousOperation.precedence >= precedence) {
          previousOperation.addInput(currentOperation);
          collapse(precedence);
        }
        else {
          stack.push(currentOperation);
        }
      };

      reset();

      return {
        push: function (number, operation) {
          error && reset();
          var stack = levels.peek();
          var lastOperation = stack.peek();
          var input = closedContext || number;
          closedContext = null;
          partialResult = Number(input);
          if ( ! lastOperation || operation.precedence > lastOperation.precedence) {
            stack.push(operation.addInput(input));
            collapse(operation.precedence);
          }
          else {
            lastOperation.addInput(input);
            collapse(operation.precedence);
            wrapLastOperation(operation);
          }
          return this;
        },
        openContext: function () {
          error && reset();
          var lastOperation = levels.peek().peek();
          if (closedContext || lastOperation && lastOperation.isSaturated()) return;
          levels.push(new Stack);
          return this;
        },
        closeContext: function (number) {
          error && reset();
          if (levels.length <= 1) return;
          var input = closedContext || number;
          var stack = levels.peek();
          var lastOperation = stack.peek();
          closedContext = new Operation(operationData.context).addInput(
            lastOperation ? (function () {
              lastOperation.addInput(input);
              collapse(0);
              return stack.pop();
            }()) : input
          );
          partialResult = Number(closedContext);
          levels.pop();
          return this;
        },
        evaluate: function (number) {
          error && reset();
          var input = closedContext || number;
          partialResult = Number(input);
          while (levels.length > 1) {
            this.closeContext(input);
          }
          var lastOperation = levels.peek().peek();
          lastOperation && lastOperation.addInput(input);
          collapse(0);
          reset();
          return this;
        },
        getPartialResult: function () {
          var _partialResult = partialResult;
          partialResult = 0;
          return _partialResult;
        },
        getCalculationString: function (collapsed) {
          var result = closedContext ? closedContext.getCalculationString('', collapsed) : '';
          for (var j = levels.length - 1; j >= 0; --j) {
            for (var i = levels[j].length - 1; i >= 0; --i) {
              result = levels[j][i].getCalculationString(result, collapsed);
            }
            if (j > 0) {
              result = '(' + result;
            }
          }
          return result;
        }
      };
     
    }());





    $.fn.addButton = function (html, className, onclick) {
      $('<div/>', {
        html: html,
        'class': 'button ' + className,
        click: onclick
      }).appendTo(this);
      return this;
    };

    var addNumberButton = function (number) {
      $numbers.addButton(number, 'number ' + (number == '.' ? 'dot' : 'number-' + number), function () {
        if ($input.text().match(/\./) && number == '.') return;
        if ($input.text() == '0' && number != '.' || $input.data('clearOnInput')) {
          $input.text('');
        }
        $input.data({clearOnInput: false});
        $input.text($input.text() + $(this).text());
      });
    };
   
    var addOperationButton = function (operation, click) {
      $operations.addButton(operation.buttonHTML, 'operation ' + operation.name, function (e) {
        click.call(this, e);
        $calculation.text(InputStack.getCalculationString());
        $collapsedCalculation.text(InputStack.getCalculationString(true));
        $input.text(InputStack.getPartialResult());
        $input.data({clearOnInput: true});
      });
    };

    var getInput = function () {
      var input = $input.text();
      return input.match(/error/i) ? 0 : parseFloat($input.text());
    };

    var toggleMemoryIndicator = function () {
      $memoryIndicator[m ? 'show' : 'hide']();
    };
   
    var i, m = 0;
    var $calculator = $('#calculator');
    var $ioField = $('<div/>', {'class': 'io-field'}).appendTo($calculator);
    var $calculation = $('<div/>', {'class': 'calculation'}).appendTo($ioField);
    var $collapsedCalculation = $('<div/>', {'class': 'collapsed-calculation'}).appendTo($ioField);
    var $input = $('<div/>', {'class': 'input', text: 0}).appendTo($ioField);
    var $keyboardInput = $('<input/>').appendTo($calculator)
      .focus().css({opacity: 0, position: 'absolute', top: 0});
    var $numbers = $('<div/>', {'class': 'numbers'}).appendTo($calculator);
    var $operations = $('<div/>', {'class': 'operations'}).appendTo($calculator);
    var $memoryIndicator = $('<div/>', {text: 'M', 'class': 'memory-indicator'}).appendTo($ioField).hide();

    $calculator.click(function () {
      $keyboardInput.focus();
    });

    $(window).keydown(function (e) {
      setTimeout(function () {
        var val = $keyboardInput.val();
        $keyboardInput.val('');
        switch (e.keyCode) {
          case 13: $('.button.evaluate').click(); break;
          case 110: case 188: case 190: $('.button.dot').click(); break;
          case 8: $('.button.del').click(); break;
          case 46: $('.button.clear-entry').click(); break;
          case 27: $('.button.clear').click(); break;
          default:
            $calculator.find('.button').each(function () {
              if (val == $(this).text()) {
                $(this).click();
              }
            });
        }
      }, 0);
    });

    $numbers.addButton('&larr;', 'del', function () {
      $input.text($input.text().replace(/.$/, ''));
      $input.text().length || $input.text('0');
    });
    $numbers.addButton('CE', 'clear-entry', function () {
      $input.text('0');
    });
    $numbers.addButton('C', 'clear', function () {
      $('#calculator .evaluate').click();
      $input.text('0');
    });
    $.each('7894561230.'.split(''), function () {
      addNumberButton(this.toString());
    });
   
    $operations.addButton('MR', 'memRecall', function () {
      $input.text(m);
    });
    $operations.addButton('MS', 'memSave', function () {
      m = getInput();
      toggleMemoryIndicator();
    });
    $operations.addButton('M+', 'memAdd', function () {
      m += getInput();
      toggleMemoryIndicator();
    });
    $operations.addButton('M-', 'memSubtract', function () {
      m -= getInput();
      toggleMemoryIndicator();
    });
    addOperationButton({buttonHTML: '(', name: 'openContext'}, function () {
      InputStack.openContext();
    });
    addOperationButton({buttonHTML: ')', name: 'closeContext'}, function () {
      InputStack.closeContext(getInput());
    });
    for (i in operationData) {
      (function (i) {
        if ( ! operationData[i].buttonHTML) return;
        addOperationButton(operationData[i], function () {
          InputStack.push(getInput(), new Operation(operationData[i]));
        });
      }(i));
    }
    addOperationButton({buttonHTML: '=', name: 'evaluate'}, function () {
      InputStack.evaluate(getInput());
    });

   
  }());


});

</script>

<style type="text/css">
  #calculator {border: 1px solid; width: 300px; overflow: auto; background: #ccc;}
  .io-field {border: 1px solid; height: 52x; margin: 5px; padding: 2px;
    text-align: right; overflow: hidden; position: relative; background: #fff;}
  .calculation, .collapsed-calculation {font-size: 10px; height: 14px; line-height: 14px;}
  .input {font-size: 16px; height: 24px; line-height: 24px;}
  .numbers {margin: 5px; width: 120px; float: left;}
  .operations {margin: 5px; width: 160px; float: left;}
  .button {width: 30px; height: 30px; padding: 0; font-size: 14px; line-height: 30px;
    text-align: center; border: 1px solid; cursor: pointer; float: left; margin: 4px; background: #fafafa;}
  .button.number-0 {width: 70px;}
  .memory-indicator {position: absolute; bottom: 10px; left: 2px; font-size: 10px;}
</style>

</head>
<body><div id="calculator"></div></body>
</html>

It’s still not very interesting, in that it just does what any old calculator can do. Since we built it with maintainability and extensibility in mind, it should be pretty easy, though, to make it do cool things not every calculator does — more on that in the next part.