MikeMcl / bignumber.js

A JavaScript library for arbitrary-precision decimal and non-decimal arithmetic
http://mikemcl.github.io/bignumber.js
MIT License
6.64k stars 741 forks source link

Imprecise calculation results #279

Closed 0x0dd4b2 closed 3 years ago

0x0dd4b2 commented 3 years ago

Actually, I run into this issue while writing unit tests for parts of my code. I am testing some calculation logic and thereby, execute the following calculation in a unit test, written in TypeScript:

import BigNumber from 'bignumber.js';

const a = new BigNumber('90');
const b = new BigNumber('0.05');
const c = new BigNumber('0.1');

const result = a.times(b.plus('1').dividedBy((new BigNumber('1')).minus(c)));

In my unit test, I have the following line

expect(result).toEqual(new BigNumber('105'));

which fails with

 - Expected  - 1
 + Received  + 1

 - "105"
 + "105.0000000000000000003"

According to wolfram alpha, the calculation should exactly result 105. So, as far as I can see, the calculation returns 105.0000000000000000003 but should return 105? I also altered the unit test, as I imagined that maybe toEqual executes some strange transformation, to

expect(result.isEqualTo(new BigNumber('105'))).toBeTruthy();

However, this test fails as well.

I'm not sure whether I misunderstood something here and this is expected behavior. In that case, I'd be thankful for a hint to some resources explaining why this is expected. Otherwise, I hope this helps fixing a potential bug in this great library.

Used version of bignumber.js is 9.0.1.

MikeMcl commented 3 years ago

There is no bug here.

Your sum is 90 * (1.05 / 0.9) and

1.05 / 0.9 = 7 / 6 which cannot be represented exactly as a decimal floating-point number as it is non-terminating:

1.1666666666666666666666666666666666666666666666666666666666666666...

With this library, the number of decimal places of the result of a division operation depends on the value of the DECIMAL_PLACES setting.

So, for example

import BigNumber from './bignumber.mjs';

const a = new BigNumber('90');
const b = new BigNumber('0.05');
const c = new BigNumber('0.1');

const dp = 20;
const expected = new BigNumber(105);

BigNumber.set({ DECIMAL_PLACES: dp })

const result = a.times(b.plus('1').dividedBy((new BigNumber('1')).minus(c)));

console.log( result.isEqualTo(expected) );                          // false
console.log( result.decimalPlaces(dp - 1).isEqualTo(expected) );    // false
console.log( result.decimalPlaces(dp - 2).isEqualTo(expected) );    // true
console.log( result.integerValue().isEqualTo(expected) );           // true
0x0dd4b2 commented 3 years ago

Oh, thanks! Somehow, I mixed it up in my head and thought that the library worked symbolic. Totally makes sense, sorry for the inconveniences!