Big Number Calculator

Every calculator has a limit on how large a number it can deal with while still producing exact results. On vintage hardware calculators, that limit presented itself in the size of the display — the actual reason for that limit lies deeper, though: a) there’s only limited memory to work with, and b) if a certain number size is not exceeded, calculations can be done a lot faster.

The reason for b) is that numbers that are small enough to fit into a processor register can be dealt with directly by the CPU on a hardware-level, instead of taking a detour through some piece of software, so it’s reasonable to limit number sizes that way, in order to ensure lightning-fast calculations. For Javascript, that means that the largest number that can be handled exactly (meaning without rounding) is 2^53, which is 9007199254740992.

That’s it — go any higher, and you won’t be dealing with exact numbers any more.

You can try that in the following example — if we add 1 to the largest exact number, it will be rounded, so it will actually stay the same:

1
2
3
4
5
6
var len = Math.pow(2, 53); // Largest Exact Number
var lenPlusOne = len + 1;

console.log('In Javascript, ' + len+ ' + 1 = ' + lenPlusOne + '.');
console.log('So, ' + len + ' and (' + len + ' + 1) are actually ' +
  (len == lenPlusOne ? 'the same number.' : 'different numbers.'));

So, what are we supposed to do if we want to do precise calculations on large numbers? Well, we could just do our calculations ourselves. With pen and paper, there’s no limit to the size of the numbers we can work with, except the size of the sheet of paper — and that’s where a) (the memory limit) comes in.

When doing pen-and-paper-calculations, you’re actually manipulating strings of digits, and that’s exactly how we are going to solve this problem: We’re not going to use (limited) built-in number types, but custom objects representing numbers of arbitrary size.

In the simplest of cases, those custom objects can, at their core, actually be just strings of digits. It might be easier, though, to handle arrays of digits. Slap on a few methods that represent mathematical operations, and we’re ready to go.

In order to figure out how exactly those mathematical operations are supposed to work when we’re not dealing with number types, but with strings or arrays of digits, we can simply code whatever we would do with pen and paper. If we care about speed, we could use other algorithms that may be significantly faster than the pen-and-paper ones.

Harvest Time

The thing is, though, that we are not even going to care about how to make it work — there are already a bunch of big number libraries for Javascript out there, so why reinvent the wheel?

In the previous part of this article, we went through quite some effort to abstract all the operator logic away from all the other stuff we wanted our calculator to do, and now it’s time to reap the benefits: We can simply reuse our OOP calculator, adjust the configuration object, so it uses the big-number-library’s operations instead of the usual ones, and be done with it.

The library I’m going to use is this one: https://github.com/silentmatt/javascript-biginteger

If you recall, we used Javascript’s Number factory in order to convert operations to results before. We can’t do that now, since results aren’t numbers any more, but custom big-number-objects, so we’re going to use the big-number-object’s toString method instead. Apart from the configuration object, that’s about the only change we will have to make to our code from before.

Since the library we are using here was made for integer calculations, I also removed a few operations that don’t make sense for integers, and added a few others, that help getting small numbers very large very fast.

Here’s the finished Big Number Calculator:

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
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Big Number Calculator</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
<script src="/demos/calculator/biginteger.js"></script>
<script type="text/javascript">

$(function () {


  var Calculator = (function () {

      var operationData = (function () {

      var bi = BigInteger;
      bi.prototype.valueOf = bi.prototype.toString;
     
      return {
        add: {
          precedence: 1,
          name: 'add',
          operation: function (a, b) {return bi.add(a, b);},
          output: function (a, b) {return a + ' + ' + b;},
          buttonHTML: '+'
        },
        subtract: {
          precedence: 1,
          name: 'subtract',
          operation: function (a, b) {return bi.subtract(a, b);},
          output: function (a, b) {return a + ' - ' + b;},
          buttonHTML: '-'
        },
        multiply: {
          precedence: 2,
          name: 'multiply',
          operation: function (a, b) {return bi.multiply(a, b);},
          output: function (a, b) {return a + ' * ' + b;},
          buttonHTML: '*'
        },
        div: {
          precedence: 2,
          name: 'divide',
          operation: function (a, b) {return bi.quotient(a, b);},
          isInvalidInput: function (a, b) {return b == 0 ? 'division by 0' : false;},
          output: function (a, b) {return a + ' / ' + b;},
          buttonHTML: 'Div'
        },
        mod: {
          precedence: 2,
          name: 'modulo',
          operation: function (a, b) {return bi.remainder(a, b);},
          output: function (a, b) {return a + ' % ' + b;},
          buttonHTML: 'Mod'
        },
        negate: {
          precedence: 4,
          singleInput: true,
          name: 'negate',
          operation: function (a) {return bi.negate(a);},
          output: function (a) {return 'negate(' + a + ')';},
          buttonHTML: '&#177;'
        },
        square: {
          precedence: 4,
          singleInput: true,
          name: 'square',
          operation: function (a) {return bi.pow(a, 2);},
          output: function (a) {return 'sqr(' + a + ')';},
          buttonHTML: 'x<sup>2</sup>'
        },
        power: {
          precedence: 3,
          name: 'power',
          operation: function (a, b) {return bi.pow(a, b);},
          output: function (a, b) {return a + ' ^ ' + b;},
          buttonHTML: 'x<sup>y</sup>'
        },
        exp10: {
          precedence: 4,
          name: 'exp10',
          operation: function (a, b) {return bi.exp10(a, b);},
          output: function (a, b) {return '(' + a + ' * 10^' + b + ')';},
          buttonHTML: '*10<sup>y</sup>'
        },
        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) 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 input.valueOf();});
        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 = currentOperation.valueOf();
        }
        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 = input.valueOf();
          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 = closedContext.valueOf();
          levels.pop();
          return this;
        },
        evaluate: function (number) {
          error && reset();
          var input = closedContext || number;
          partialResult = input.valueOf();
          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.toString();
        },
        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 : BigInteger($input.text());
    };

    var i;
    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);

    $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());
    });
   
    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; overflow: auto; background: #ccc;}
  .io-field {border: 1px solid; height: 156px; margin: 5px; padding: 2px;
    text-align: right; overflow: hidden; position: relative; background: #fff;}
  .calculation, .collapsed-calculation {font-size: 10px; height: 56px; line-height: 14px; overflow: auto;}
  .input {font-size: 16px; height: 48px; line-height: 24px; overflow: auto;}
  .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;}
</style>

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

This is a bit raw, in that it won’t prevent you from doing insanely huge calculations that will lock up your CPU or kill your browser. I didn’t bother putting any time into a nice presentation of long calculations, either. The point, though, is this:

Because of the way we approached the general problem of writing a calculator, it was trivially easy to adapt it to a rather specific need: exact calculations with arbitrarily large numbers. That’s the beauty of an OOP approach: Put some thought into it in the beginning, and you might just end up with something you can reuse for all sorts of related problems.

Although there’s almost never a need to actually write a calculator in real life (except for homework assignments), I hope you enjoyed the article and got the gist of it — not how to calculate stuff, but how to go about solving a particular problem in a way that keeps your code maintainable and extensible in the long run. If you have any feedback, feel free to shoot me an email at feedback@reallifejs.com.