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: '±' }, 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('←', '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.