diff --git a/lib/big-number.js b/lib/big-number.js index 72f6335..92e1fcd 100644 --- a/lib/big-number.js +++ b/lib/big-number.js @@ -11,6 +11,67 @@ !(function() { 'use strict'; + /** + * The base to which we hold the big number + * + * This must be set to no more than the square root of the maximum native integer that JS can represent accurately + * It can be any number from base 2 up this maximum + * The larger the number the more efficient the big number arithmetic will be + * However, for division, the choice of number also has a significant impact. Infact for maximum efficiency + * the base should have as many common factors as possible. This is maximised by having the base to be a power + * of 2. JS typically holds integers accurately to at least up to 2^50 so a value of 2^25 would be the optimal + * base to use. Setting the base to a prime number will have the worst effect on performance so far as division + * is concerned. + * + * The original version was hard coded with a base of 10. If we set our base to this now it will have similar + * performance, though the division routine has been optimised such that even with this base it could be up + * to twice as fast. + * + * Even with the base set to 2 (i.e. the numbers are held in binary!) the arithmetic is surprisingly fast. + * But nothing like as fast as with a base of 2^25 + * + * @type {number} + */ + var base=Math.pow(2,25); + + /* + * Factorise the base - these then become the most efficient way to segment the division process + * Essentially, the most efficient base is the one with the most factors + * Which means that a base of a power of 2 is the best choice + * + * This is somewhat over the top, but is a good exercise to ensure that division is performed + * with maximum efficiency whatever the base :-) + */ + var factors=(function() { + var result=[]; + var primes=[]; + var factorisingbase=base; + var lastprime=2; + + // Check number for prime (assumes previous primes have been found) + function isprime(prime) { + // Only need to test divisors so far as the square root of the number under test + for (var i=0; i1) { + if (factorisingbase%lastprime==0) { + result.push(lastprime); + factorisingbase=Math.floor(factorisingbase/lastprime); + } + else { + do { + lastprime=(lastprime==2 ? 3 : lastprime+2); + } while (!isprime(lastprime)); + primes.push(lastprime); + } + } + return result; + })(); + // Helper function which tests if a given character is a digit var testDigit = function(digit) { return (/^\d$/.test(digit)); @@ -33,68 +94,112 @@ }; var errors = { - "invalid": "Invalid Number", - "division by zero": "Invalid Number - Division By Zero" + 'invalid': 'Invalid Number', + 'division by zero': 'Invalid Number - Division By Zero' }; - // Constructor function which creates a new BigNumber object - // from an integer, a string, an array or other BigNumber object + + /* + * Constructor function which creates a new BigNumber object from an integer, a string, an array or other BigNumber object + * + * IMPORTANT NOTE: Once we allow for any base, the routines to initiate a number from a string of decimal numbers + * and to output a number in decimal form (.val()) now use big number arithmetic. This has potential to get + * us into trouble if we are to build numbers from within internal routines that are already manipulating numbers. It can cause + * a stack overflow. This situation is now avoided by optimising the initialisation to handle more specifically + * (and efficiently) numbers constructed from existing BigNumber objects or native numbers. + */ function BigNumber(initialNumber) { - var index; if (!(this instanceof BigNumber)) { return new BigNumber(initialNumber); } - this.number = [] + this.number = []; this.sign = 1; this.rest = 0; + // No parameter initialises the number to zero if (!initialNumber) { this.number = [0]; return; } + // A big number parameter initialises this number to the same value + if (initialNumber instanceof BigNumber) { + this.number = initialNumber.number.slice(); + this.sign = initialNumber.sign; + this.rest = initialNumber.rest; + return; + } + + // A native number makes for a fast initialisation of the number + if (typeof initialNumber=='number') { + this.sign=initialNumber<0 ? -1 : 1; + initialNumber=Math.abs(initialNumber); + while (initialNumber>0) { + this.number.push(initialNumber%base); + initialNumber=Math.floor(initialNumber/base); + } + return; + } + + // Otherwise, now create the number from a String or Array + var index; + var multipleoftens=BigNumber(1); + var ten=BigNumber(10); + + // Method removed from public API and made local within this context + function addDigit(digit) { + if (testDigit(digit)) { + this.add(BigNumber(digit).mult(multipleoftens)); + multipleoftens.mult(ten); + return true; + } + return false; + } + // The initial number can be an array or object // e.g. array : [3,2,1], ['+',3,2,1], ['-',3,2,1] // number : 312 // string : '321', '+321', -321' // BigNumber : BigNumber(321) // Every character except the first must be a digit - + var sign=1; if (isArray(initialNumber)) { if (initialNumber.length && initialNumber[0] === '-' || initialNumber[0] === '+') { - this.sign = initialNumber[0] === '+' ? 1 : -1; + sign = initialNumber[0] === '+' ? 1 : -1; initialNumber.shift(0); } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!this.addDigit(initialNumber[index])) + if (!addDigit.call(this,initialNumber[index])) { + this.number = errors['invalid']; return; + } } } else { initialNumber = initialNumber.toString(); if (initialNumber.charAt(0) === '-' || initialNumber.charAt(0) === '+') { - this.sign = initialNumber.charAt(0) === '+' ? 1 : -1; + sign = initialNumber.charAt(0) === '+' ? 1 : -1; initialNumber = initialNumber.substring(1); } for (index = initialNumber.length - 1; index >= 0; index--) { - if (!this.addDigit(parseInt(initialNumber.charAt(index), 10))) { + if (!addDigit.call(this,parseInt(initialNumber.charAt(index), 10))) { + this.number = errors['invalid']; return; } } } - }; - - BigNumber.prototype.addDigit = function(digit) { - if (testDigit(digit)) { - this.number.push(digit); - } else { - this.number = errors['invalid']; - return false; - } + this.sign=sign; + } - return this; + // Statistics on the number of operations performed (useful for checking performance) + BigNumber.stats={ + divs: {count: 0,iterations: 0}, + mults: {count: 0,iterations: 0}, + adds: {count: 0,iterations: 0}, + subs: {count: 0,iterations: 0}, + pows: {count: 0,iterations: 0} }; // returns: @@ -210,14 +315,16 @@ // adds two positive BigNumbers BigNumber._add = function(a, b) { var index; + var remainder = 0; var length = Math.max(a.number.length, b.number.length); for (index = 0; index < length || remainder > 0; index++) { - a.number[index] = (remainder += (a.number[index] || 0) + (b.number[index] || 0)) % 10; - remainder = Math.floor(remainder / 10); + a.number[index] = (remainder += (a.number[index] || 0) + (b.number[index] || 0)) % base; + remainder = Math.floor(remainder / base); + BigNumber.stats.adds.iterations++; } - + BigNumber.stats.adds.count++; return a.number; }; @@ -230,7 +337,8 @@ for (index = 0; index < length; index++) { a.number[index] -= (b.number[index] || 0) + remainder; - a.number[index] += (remainder = (a.number[index] < 0) ? 1 : 0) * 10; + a.number[index] += (remainder = (a.number[index] < 0) ? 1 : 0) * base; + BigNumber.stats.subs.iterations++; } // Count the zeroes which will be removed index = 0; @@ -241,6 +349,7 @@ if (index > 0) { a.number.splice(-index); } + BigNumber.stats.subs.count++; return a.number; }; @@ -264,12 +373,14 @@ // multiply the numbers for (index = 0; index < this.number.length; index++) { for (remainder = 0, givenNumberIndex = 0; givenNumberIndex < bigNumber.number.length || remainder > 0; givenNumberIndex++) { - result[index + givenNumberIndex] = (remainder += (result[index + givenNumberIndex] || 0) + this.number[index] * (bigNumber.number[givenNumberIndex] || 0)) % 10; - remainder = Math.floor(remainder / 10); + result[index + givenNumberIndex] = (remainder += (result[index + givenNumberIndex] || 0) + this.number[index] * (bigNumber.number[givenNumberIndex] || 0)) % base; + remainder = Math.floor(remainder / base); + BigNumber.stats.mults.iterations++; } } this.number = result; + BigNumber.stats.mults.count++; return this; }; @@ -284,6 +395,8 @@ var length; var result = []; var rest = BigNumber(); + var nativebigNumber=bigNumber.number.length==1 ? bigNumber.number[0] : 0; + var nativerest=0; // test if one of the numbers is zero if (bigNumber.isZero()) { @@ -301,12 +414,31 @@ return this; for (index = this.number.length - 1; index >= 0; index--) { - rest.multiply(10); - rest.number[0] = this.number[index]; - result[index] = 0; - while (bigNumber.lte(rest)) { - result[index]++; - rest.subtract(bigNumber); + + if (nativebigNumber) { + // If we are dividing by a small number (less than our base) then we can do this division step natively + // which is much faster + nativerest*=base; + nativerest+=this.number[index]; + result[index]=Math.floor(nativerest/nativebigNumber); + nativerest-=(result[index]*nativebigNumber); + } else { + // Otherwise we need to use BigNumber arithmetic + var digit=this.number[index]; + result[index] = 0; + // Segment the division by the factors of the base + var divisor=base; + for (var i=0; i 0) { result.splice(-index); } - - this.rest = rest; + if (nativebigNumber) this.rest=BigNumber(nativerest); + else this.rest = rest; this.number = result; + BigNumber.stats.divs.count++; return this; }; @@ -346,6 +479,7 @@ this.number = [1]; while (number > 0) { + BigNumber.stats.pows.iterations++; if (number % 2 === 1) { this.multiply(bigNumber); number--; @@ -354,7 +488,7 @@ bigNumber.multiply(bigNumber); number = Math.floor(number / 2); } - + BigNumber.stats.pows.count++; return this; }; @@ -376,22 +510,37 @@ return true; }; + // Gets a single decimal digit value of the number assuming it is less than 10 (used by the toString() function) + BigNumber.prototype._singleDigit=function() { + var digit=0; + var multiplier=1; + for (var i=0; i= 0; index--) { - str += this.number[index]; + var ten=BigNumber(10); + var clone=BigNumber(this).abs(); + while(!clone.isZero()) { + str=clone.div(ten).rest._singleDigit()+str; } - + if (str.length==0) str='0'; return (this.sign > 0) ? str : ('-' + str); }; - // Use shorcuts for functions names + // Use shortcuts for functions names BigNumber.prototype.plus = BigNumber.prototype.add; BigNumber.prototype.minus = BigNumber.prototype.subtract; BigNumber.prototype.div = BigNumber.prototype.divide; diff --git a/test/test.js b/test/test.js index 59c92fc..4dca6d7 100644 --- a/test/test.js +++ b/test/test.js @@ -292,4 +292,21 @@ describe('BigNumber.js', function () { BigNumber(5).plus(97).minus(53).plus(434).multiply(5435423).add(321453).multiply(21).div(2).val().should.equal("27569123001"); }); }); + + describe('very big numbers', function () { + it('should test main arithmetic on very large integers', function () { + var a=BigNumber("43216789895328312675790036721362674782378923750950234212368965373910384654"); + var b=BigNumber("85764636478697019837453424264758699685764562524345456576868"); + var x = BigNumber(a); + x.add(b).val().should.equal("43216789895328398440426515418382512235803188509649919976931489719366961522"); + x = BigNumber(a); + x.minus(b).val().should.equal("43216789895328226911153558024342837328954658992250548447806441028453807786"); + x = BigNumber(a); + x.multiply(b).val().should.equal("3706472275149059366968352499608225426876935061337924498923340460238964318011366196170940767807131779369992004353943009853967998583672"); + x = BigNumber(a); + x.divide(b).val().should.equal("503899878431396"); + x = BigNumber(a); + x.divide(b).rest.val().should.equal("13662369696833870468897163678348569097398678349966371836926"); + }); + }); });