diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/BigDecimal.js b/BigDecimal.js index 13c968e..415f4bd 100644 --- a/BigDecimal.js +++ b/BigDecimal.js @@ -80,7 +80,7 @@ function naturalLogarithm(n) { var s = n.toString(16); var p = Math.floor(Math.log((Number.MAX_SAFE_INTEGER + 1) / 32 + 0.5) / Math.log(2)); var l = Math.floor(p / 4); - return Math.log(Number('0x' + s.slice(0, l)) / Math.pow(2, 4 * l)) + 4 * Math.log(2) * s.length; + return Math.log(Number("0x" + s.slice(0, l)) / Math.pow(2, 4 * l)) + 4 * Math.log(2) * s.length; } function digits(a) { // floor(log10(abs(a.significand))) + 1 var n = bigIntMax(bigIntAbs(a.significand), 1n); @@ -327,7 +327,7 @@ function abs(a) { } function sign(x) { - return BigDecimal.lessThan(x, BigDecimal.BigDecimal(0)) ? BigDecimal.BigDecimal(-1) : (BigDecimal.greaterThan(x, BigDecimal.BigDecimal(0)) ? BigDecimal.BigDecimal(1) : BigDecimal.BigDecimal(0)); + return BigDecimal.lessThan(x, BigDecimal.BigDecimal(0)) ? BigDecimal.BigDecimal(-1) : (BigDecimal.greaterThan(x, BigDecimal.BigDecimal(0)) ? BigDecimal.BigDecimal(+1) : BigDecimal.BigDecimal(0)); } function significandDigits(a) { @@ -361,6 +361,9 @@ function tryToMakeCorrectlyRounded(specialValue, f, name) { var i = 0; var error = BigDecimal.BigDecimal(0); do { + if (i > 4 && rounding.maximumSignificantDigits != null && rounding.roundingMode === "half-even" && name !== "sin" && name !== "cos") { + throw new Error(); + } i += 1; var internalRounding = { maximumSignificantDigits: Math.ceil(Math.max(rounding.maximumSignificantDigits || (rounding.maximumFractionDigits + 1 + getExpectedResultIntegerDigits(x) - 1), significandDigits(x)) * Math.cbrt(Math.pow(2, i - 1))) + 2, @@ -372,12 +375,9 @@ function tryToMakeCorrectlyRounded(specialValue, f, name) { //if (i > 0) { //console.log(i, f.name, x + "", result + "", error + "", BigDecimal.round(BigDecimal.subtract(result, error), rounding) + "", BigDecimal.round(BigDecimal.add(result, error), rounding) + ""); //} - if (i > 10 && rounding.maximumSignificantDigits != null) { - throw new Error(); - } } while (!BigDecimal.equal(BigDecimal.round(BigDecimal.subtract(result, error), rounding), BigDecimal.round(BigDecimal.add(result, error), rounding))); if (i > 1) { - console.log(i); + //console.debug(i, name); } return BigDecimal.round(result, rounding); }; @@ -389,7 +389,7 @@ function sqrt(x, rounding) { var result = BigDecimal.divide(x, BigDecimal.BigDecimal(2)); while (BigDecimal.lessThan(result, lastResult)) { lastResult = result; - result = BigDecimal.divide(BigDecimal.add(BigDecimal.divide(BigDecimal.BigDecimal(x), result, rounding), result), BigDecimal.BigDecimal(2), rounding); + result = BigDecimal.divide(BigDecimal.add(BigDecimal.divide(x, result, rounding), result), BigDecimal.BigDecimal(2), rounding); } return result; } @@ -417,7 +417,7 @@ BigDecimal.log = tryToMakeCorrectlyRounded(1, function log(x, rounding) { f = BigDecimal.multiply(f, BigDecimal.BigDecimal(10)); } if (k !== 0n) { - return BigDecimal.add(BigDecimal.log(f, internalRounding), BigDecimal.multiply(BigDecimal.BigDecimal(2n * k), BigDecimal.log(BigDecimal.BigDecimal(sqrt(BigDecimal.BigDecimal(10), internalRounding)), internalRounding))); + return BigDecimal.add(BigDecimal.log(f, internalRounding), BigDecimal.multiply(BigDecimal.BigDecimal(2n * k), BigDecimal.log(sqrt(BigDecimal.BigDecimal(10), internalRounding), internalRounding))); } } //! log(x) = log((1 + g) / (1 - g)) = 2*(g + g**3/3 + g**5/5 + ...) @@ -441,13 +441,15 @@ BigDecimal.log = tryToMakeCorrectlyRounded(1, function log(x, rounding) { BigDecimal.exp = tryToMakeCorrectlyRounded(0, function exp(x, rounding) { //! k = round(x / ln(10)); //! exp(x) = exp(x - k * ln(10) + k * ln(10)) = exp(x - k * ln(10)) * 10**k - var k = BigDecimal.divide(x, BigDecimal.BigDecimal(BigDecimal.divide(BigDecimal.BigDecimal(2302585092994046), BigDecimal.BigDecimal(1000000000000000))), {maximumFractionDigits: 0, roundingMode: "half-even"}); + var k = BigDecimal.divide(x, BigDecimal.divide(BigDecimal.BigDecimal(2302585092994046), BigDecimal.BigDecimal(1000000000000000)), {maximumFractionDigits: 0, roundingMode: "half-even"}); var internalRounding = { maximumSignificantDigits: rounding.maximumSignificantDigits + Math.ceil(Math.log(rounding.maximumSignificantDigits + 0.5) / Math.log(10)), roundingMode: "half-even" }; if (!BigDecimal.equal(k, BigDecimal.BigDecimal(0))) { - var r = BigDecimal.subtract(x, BigDecimal.multiply(k, BigDecimal.log(BigDecimal.BigDecimal(10), {maximumSignificantDigits: internalRounding.maximumSignificantDigits + Number(getCountOfDigits(k)), roundingMode: "half-even"}))); + var log10 = BigDecimal.log(BigDecimal.BigDecimal(10), {maximumSignificantDigits: internalRounding.maximumSignificantDigits + Number(getCountOfDigits(k)), roundingMode: "half-even"}); + k = BigDecimal.divide(x, log10, {maximumFractionDigits: 0, roundingMode: "half-even"}); + var r = BigDecimal.subtract(x, BigDecimal.multiply(k, log10)); return BigDecimal.multiply(BigDecimal.exp(r, internalRounding), exponentiate(BigDecimal.BigDecimal(10), BigDecimal.toBigInt(k))); } // https://en.wikipedia.org/wiki/Exponential_function#Computation @@ -470,7 +472,7 @@ function divideByHalfOfPI(x, rounding) { // x = k*pi/2 + r + 2*pi*n, where |r| < throw new RangeError(); } if (BigDecimal.greaterThan(x, BigDecimal.divide(BigDecimal.BigDecimal(785398163397448), BigDecimal.BigDecimal(1000000000000000)))) { - var halfOfPi = BigDecimal.multiply(BigDecimal.BigDecimal(2), BigDecimal.atan(BigDecimal.BigDecimal(1), {maximumSignificantDigits: rounding.maximumSignificantDigits + Number(getCountOfDigits(x)), roundingMode: "half-even"})); + var halfOfPi = BigDecimal.multiply(BigDecimal.BigDecimal(2), BigDecimal.atan(BigDecimal.BigDecimal(1), {maximumSignificantDigits: rounding.maximumSignificantDigits + Number(getCountOfDigits(x)) + 1, roundingMode: "half-even"})); var i = BigDecimal.divide(x, halfOfPi, {maximumFractionDigits: 0, roundingMode: "half-even"}); var remainder = BigDecimal.subtract(x, BigDecimal.multiply(i, halfOfPi)); return {remainder: remainder, k: (Number(BigDecimal.toBigInt(i) % 4n) + 4) % 4}; @@ -511,7 +513,7 @@ BigDecimal.sin = tryToMakeCorrectlyRounded(0, function (x, rounding) { sum = BigDecimal.add(sum, term, internalRounding); } return BigDecimal.multiply(a, sum); -}); +}, "sin"); BigDecimal.cos = tryToMakeCorrectlyRounded(0, function (x, rounding) { if (BigDecimal.lessThan(x, BigDecimal.BigDecimal(0))) { @@ -546,14 +548,14 @@ BigDecimal.cos = tryToMakeCorrectlyRounded(0, function (x, rounding) { sum = BigDecimal.add(sum, term, internalRounding); } return sum; -}); +}, "cos"); BigDecimal.atan = tryToMakeCorrectlyRounded(0, function (x, rounding) { if (BigDecimal.greaterThan(abs(x), BigDecimal.BigDecimal(1))) { var halfOfPi = BigDecimal.multiply(BigDecimal.atan(BigDecimal.BigDecimal(1), rounding), BigDecimal.BigDecimal(2)); return BigDecimal.multiply(sign(x), BigDecimal.subtract(halfOfPi, BigDecimal.atan(BigDecimal.divide(BigDecimal.BigDecimal(1), abs(x), rounding), rounding))); } - // https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Infinite_series + // https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#:~:text=Alternatively,%20this%20can%20be%20expressed%20as var internalRounding = { maximumSignificantDigits: rounding.maximumSignificantDigits + Math.ceil(Math.log(rounding.maximumSignificantDigits + 0.5) / Math.log(10)), roundingMode: "half-even" @@ -564,14 +566,14 @@ BigDecimal.atan = tryToMakeCorrectlyRounded(0, function (x, rounding) { var lastSum = BigDecimal.BigDecimal(0); while (!BigDecimal.equal(lastSum, sum)) { n += 1; - term = BigDecimal.multiply(term, BigDecimal.BigDecimal((2 * n) * (2 * n))); - term = BigDecimal.divide(term, BigDecimal.BigDecimal((2 * n) * (2 * n + 1)), internalRounding); + term = BigDecimal.multiply(term, BigDecimal.BigDecimal(2 * n)); + term = BigDecimal.divide(term, BigDecimal.BigDecimal(2 * n + 1), internalRounding); term = BigDecimal.multiply(term, BigDecimal.multiply(x, x)); term = BigDecimal.divide(term, BigDecimal.add(BigDecimal.BigDecimal(1), BigDecimal.multiply(x, x)), internalRounding); lastSum = sum; sum = BigDecimal.add(sum, term, internalRounding); } return BigDecimal.multiply(x, sum); -}); +}, "atan"); export default BigDecimal; diff --git a/BigDecimalByDecimal.js.js b/BigDecimalByDecimal.js.js new file mode 100644 index 0000000..63b468d --- /dev/null +++ b/BigDecimalByDecimal.js.js @@ -0,0 +1,112 @@ + +import Decimal from './node_modules/decimal.js/decimal.mjs'; + +function rm(roundingMode) { + if (roundingMode === 'floor') { + return Decimal.ROUND_FLOOR; + } + if (roundingMode === 'ceil') { + return Decimal.ROUND_CEIL; + } + if (roundingMode === 'half-even') { + return Decimal.ROUND_HALF_EVEN; + } + if (roundingMode === 'half-up') { + return Decimal.ROUND_HALF_UP; + } + if (roundingMode === 'half-down') { + return Decimal.ROUND_HALF_DOWN; + } + throw new RangeError('rounding mode: ' + roundingMode); +} + +function setRounding(rounding) { + if (rounding != null) { + if (rounding.maximumFractionDigits != undefined) { + throw new RangeError("rounding to maximumFractionDigits is not supported"); + } + Decimal.precision = rounding.maximumSignificantDigits; + Decimal.rounding = rm(rounding.roundingMode); + } else { + Decimal.precision = 1e+9; + } +} + +function BigDecimal() { +} +BigDecimal.BigDecimal = function (value) { + return Decimal(typeof value === 'number' ? value : value.toString()); +}; +BigDecimal.toBigInt = function (a) { + return BigInt(a.toFixed(0)); +}; +BigDecimal.toNumber = function (a) { + return a.toNumber(); +}; +BigDecimal.unaryMinus = function (a) { + return a.negated(); +}; +BigDecimal.add = function (a, b, rounding) { + setRounding(rounding); + return a.add(b); +}; +BigDecimal.subtract = function (a, b, rounding) { + setRounding(rounding); + return a.sub(b); +}; +BigDecimal.multiply = function (a, b, rounding) { + setRounding(rounding); + return a.mul(b); +}; +BigDecimal.divide = function (a, b, rounding) { + if (rounding == null) { + //throw new RangeError(); + setRounding(null); // it is working for exact divisions and throws an exception for inexact, seems + } else { + setRounding(rounding); + } + return a.div(b); +}; + +BigDecimal.lessThan = function (a, b) { + return a.lt(b); +}; +BigDecimal.greaterThan = function (a, b) { + return a.gt(b); +}; +BigDecimal.equal = function (a, b) { + return a.eq(b); +}; + +BigDecimal.cos = function (a, rounding) { + setRounding(rounding); + return a.cos(); +}; +BigDecimal.sin = function (a, rounding) { + setRounding(rounding); + return a.sin(); +}; +BigDecimal.exp = function (a, rounding) { + setRounding(rounding); + return a.exp(); +}; +BigDecimal.log = function (a, rounding) { + setRounding(rounding); + return a.ln(); +}; +BigDecimal.atan = function (a, rounding) { + setRounding(rounding); + return a.atan(); +}; + +BigDecimal.round = function (a, rounding) { + if (rounding.maximumFractionDigits != undefined) { + return a.toDecimalPlaces(rounding.maximumFractionDigits, rm(rounding.roundingMode)); + } + if (rounding.maximumSignificantDigits == undefined) { + throw new RangeError('rounding without maximumSignificantDigits is not supported'); + } + return a.toSignificantDigits(rounding.maximumSignificantDigits, rm(rounding.roundingMode)); +}; + +export default BigDecimal; diff --git a/package.json b/package.json index 292f8ee..bc15655 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yaffle/bigdecimal", - "version": "1.0.2", + "version": "1.0.3", "description": "Arbitrary precision decimal arithmetic library. Polyfill for decimal proposal. Implemented on the top of BigInt.", "main": "BigDecimal.js", "scripts": { @@ -26,5 +26,8 @@ "url": "https://github.com/Yaffle/BigDecimal/issues" }, "homepage": "https://github.com/Yaffle/BigDecimal#readme", - "type": "module" + "type": "module", + "devDependencies": { + "decimal.js": "^10.2.0" + } } diff --git a/tests.js b/tests.js index b36103b..f069197 100644 --- a/tests.js +++ b/tests.js @@ -1,9 +1,5 @@ -// The library bigdecimal.min.mjs can be found at https://github.com/MikeMcl/bignumber.js/issues/256#issuecomment-594232313 . - -import BigDecimal2 from './bigdecimal.min.mjs'; -//import BigDecimal from './BigDecimalByDecimal.js.js'; -//import BigDecimal from './bigdecimal.min.mjs'; +import BigDecimal2 from './BigDecimalByDecimal.js.js'; import BigDecimal from './BigDecimal.js'; console.time(); @@ -85,6 +81,8 @@ console.assert(BigDecimal.divide(BigDecimal.BigDecimal('6e-6'), BigDecimal.BigDe // console.time(); exponentiate(BigDecimal.BigDecimal(10), 1000000n); console.timeEnd(); //console.assert(BigDecimal.divide(BigDecimal.BigDecimal(1), BigDecimal.BigDecimal(10), {maximumSignificantDigits: 1e15}).toString(), '?'); // performance //console.assert(BigDecimal.divide(BigDecimal.BigDecimal(1), BigDecimal.BigDecimal(10), {maximumFractionDigits: 1e15}).toString(), '?'); // performance +//console.assert(BigDecimal.exp(BigDecimal.BigDecimal('1e+100'), { maximumSignificantDigits: 1, roundingMode: 'half-even' }).toString() === '2e+4342944819032518276511289189166050822943970058036665661144537831658646492088707747292249493384317483'); +//console.assert(BigDecimal.exp(BigDecimal.BigDecimal('-1e+100'), { maximumSignificantDigits: 1, roundingMode: 'half-even' }).toString() === '7e-4342944819032518276511289189166050822943970058036665661144537831658646492088707747292249493384317484'); // fromString //console.assert(BigDecimal.BigDecimal(' +1.2e+3 ').toString() === '120'); @@ -135,6 +133,7 @@ console.assert(BigDecimal.sin(BigDecimal.BigDecimal(0), { maximumSignificantDigi console.assert(BigDecimal.sin(BigDecimal.BigDecimal(0), { maximumSignificantDigits: 16, roundingMode: 'half-even' }).toString() === '0'); console.assert(BigDecimal.sin(BigDecimal.BigDecimal(0), { maximumSignificantDigits: 16, roundingMode: 'floor' }).toString() === '0'); console.assert(BigDecimal.equal(BigDecimal.sin(BigDecimal.BigDecimal(-2), { maximumSignificantDigits: 16, roundingMode: 'floor' }), BigDecimal.unaryMinus(BigDecimal.sin(BigDecimal.BigDecimal(2), { maximumSignificantDigits: 16, roundingMode: 'ceil' })))); +//console.assert(BigDecimal.sin(BigDecimal.BigDecimal('25e-10000'), { maximumSignificantDigits: 1, roundingMode: 'half-even' }).toString() === '?'); // BigDecimal.cos console.assert(BigDecimal.cos(BigDecimal.BigDecimal(1), { maximumSignificantDigits: 16, roundingMode: 'half-even' }).toString() === '0.5403023058681397'); @@ -143,6 +142,7 @@ console.assert(BigDecimal.cos(BigDecimal.BigDecimal(4), { maximumSignificantDigi console.assert(BigDecimal.cos(BigDecimal.BigDecimal(0), { maximumSignificantDigits: 16, roundingMode: 'ceil' }).toString() === '1'); console.assert(BigDecimal.cos(BigDecimal.BigDecimal(0), { maximumSignificantDigits: 16, roundingMode: 'half-even' }).toString() === '1'); console.assert(BigDecimal.cos(BigDecimal.BigDecimal(0), { maximumSignificantDigits: 16, roundingMode: 'floor' }).toString() === '1'); +console.assert(BigDecimal.cos(BigDecimal.BigDecimal('8e+9'), { maximumSignificantDigits: 9, roundingMode: 'floor' }).toString() === '-0.0930906136'); // bug // BigDecimal.atan console.assert(BigDecimal.multiply(BigDecimal.BigDecimal(4), BigDecimal.atan(BigDecimal.BigDecimal(1), { maximumSignificantDigits: 16, roundingMode: 'half-even' })).toString() === '3.1415926535897932'); @@ -157,56 +157,6 @@ console.assert(BigDecimal.equal(BigDecimal.atan(BigDecimal.BigDecimal(-2), { max //BigDecimal2 -BigDecimal2.BigDecimal2 = BigDecimal2; -BigDecimal2.toNumber = function (d) { - return Number(d.toString()); -}; -BigDecimal2.unaryMinus = function (d) { - return BigDecimal2.sub(BigDecimal2(0), d); -}; -var wrap = function (f, w) { - return function (a, b, rounding) { - if (rounding != null) { - var abs = function (d) { - return d['<'](BigDecimal2(0)) ? BigDecimal2.sub(BigDecimal2(0), d) : d; - }; - var sign = function (d) { - return d['<'](BigDecimal2(0)) ? -1 : +1; - }; - var cc = w === '*' || w === '/' ? (a['<'](BigDecimal2.BigDecimal2(0)) ? -1 : +1) * (b['<'](BigDecimal2.BigDecimal2(0)) ? -1 : +1) : (abs(a)['>'](abs(b)) ? sign(a) : sign(b) * (w === '-' ? -1 : +1)); - var roundingMode = rounding.roundingMode; - var mode = roundingMode === 'half-even' ? BigDecimal2.ROUND_HALF_EVEN : (roundingMode === 'ceil' && cc > 0 || roundingMode === 'floor' && cc < 0 ? BigDecimal2.ROUND_UP : BigDecimal2.ROUND_DOWN); - rounding = Object.assign({}, rounding, { - roundingMode: mode - }); - } - return f(a, b, rounding); - }; -}; -BigDecimal2.add = wrap(BigDecimal2.add, '+'); -BigDecimal2.subtract = wrap(BigDecimal2.sub, '-'); -BigDecimal2.multiply = wrap(BigDecimal2.mul, '*'); -BigDecimal2.divide = wrap(BigDecimal2.div, '/'); -BigDecimal2.lessThan = function (a, b) { - return a['<'](b); -}; -BigDecimal2.greaterThan = function (a, b) { - return a['>'](b); -}; -BigDecimal2.equal = function (a, b) { - return a['=='](b); -}; -var round = BigDecimal2.round; -BigDecimal2.round = function (d, rounding) { - var cc = d['<'](BigDecimal2(0)) ? -1 : +1; - var roundingMode = rounding.roundingMode; - var mode = roundingMode === 'half-up' ? BigDecimal2.ROUND_HALF_UP : (roundingMode === 'half-even' ? BigDecimal2.ROUND_HALF_EVEN : (roundingMode === 'ceil' && cc > 0 || roundingMode === 'floor' && cc < 0 ? BigDecimal2.ROUND_UP : BigDecimal2.ROUND_DOWN)); - rounding = Object.assign({}, rounding, { - roundingMode: mode - }); - return round(d, rounding); -}; - // random tests: function randomInteger() { var max = 10; @@ -214,23 +164,16 @@ function randomInteger() { return Math.floor(Math.random() * (max + min)) - min; } -for (var c = 0; c < 10; c += 1) { +for (var c = 0; c < 1000; c += 1) { var aValue = (randomInteger() + 'e+' + randomInteger()).replace(/\+\-/g, '-'); var bValue = (randomInteger() + 'e+' + randomInteger()).replace(/\+\-/g, '-'); var a = BigDecimal.BigDecimal(aValue); var b = BigDecimal.BigDecimal(bValue); - var operations = 'add subtract multiply divide'.split(' '); + var operations = 'add subtract multiply divide log exp sin cos atan'.split(' '); var operation = operations[(randomInteger() % operations.length + operations.length) % operations.length]; //var roundingType = randomInteger() % 2 === 0 ? 'maximumSignificantDigits' : 'maximumFractionDigits'; var roundingType = 'maximumSignificantDigits'; - try { - BigDecimal.round(BigDecimal.BigDecimal('1.5'), { maximumFractionDigits: 1, roundingMode: 'floor' }); - } catch (error) { - if (roundingType === 'maximumFractionDigits') { - roundingType = 'maximumSignificantDigits'; - } - } - var roundingModes = 'ceil floor half-even'.split(' '); + var roundingModes = 'ceil floor half-even half-up half-down'.split(' '); var roundingMode = roundingModes[(randomInteger() % roundingModes.length + roundingModes.length) % roundingModes.length]; var decimalDigits = randomInteger(); var rounding = { @@ -239,10 +182,21 @@ for (var c = 0; c < 10; c += 1) { maximumFractionDigits: roundingType === 'maximumFractionDigits' ? Math.max(0, decimalDigits) : undefined }; var zero = BigDecimal.BigDecimal('0'); - if (operation !== 'divide' || !BigDecimal.equal(b, zero)) { - var cc = BigDecimal[operation](a, b, rounding); - var expected = BigDecimal2[operation](BigDecimal2.BigDecimal2(aValue), BigDecimal2.BigDecimal2(bValue), rounding); + if (operation === 'atan') { + aValue = '1'; + a = BigDecimal.BigDecimal(1); + } + if ((operation !== 'divide' || !BigDecimal.equal(b, zero)) && (operation !== 'log' || BigDecimal.greaterThan(a, zero))) { + var cc = /^(log|exp|sin|cos|atan)$/.test(operation) ? BigDecimal[operation](a, rounding) : BigDecimal[operation](a, b, rounding); + var expected = /^(log|exp|sin|cos|atan)$/.test(operation) ? BigDecimal2[operation](BigDecimal2.BigDecimal(aValue), rounding) : BigDecimal2[operation](BigDecimal2.BigDecimal(aValue), BigDecimal2.BigDecimal(bValue), rounding); + if (cc.exponent > 9000000000000000n) { + cc = 'Infinity'; + } + if (cc.exponent < -9000000000000000n) { + cc = '0'; + } if (cc.toString() !== expected.toString()) { + console.log(cc.toString(), expected.toString()); throw new Error(c); } }